Browse code

f5 vxlan integration with sdn

Rajat Chopra authored on 2016/09/21 08:13:53
Showing 23 changed files
... ...
@@ -19713,6 +19713,8 @@ _openshift_infra_f5-router()
19713 19713
     local_nonpersistent_flags+=("--f5-https-vserver=")
19714 19714
     flags+=("--f5-insecure")
19715 19715
     local_nonpersistent_flags+=("--f5-insecure")
19716
+    flags+=("--f5-internal-address=")
19717
+    local_nonpersistent_flags+=("--f5-internal-address=")
19716 19718
     flags+=("--f5-partition-path=")
19717 19719
     local_nonpersistent_flags+=("--f5-partition-path=")
19718 19720
     flags+=("--f5-password=")
... ...
@@ -19721,6 +19723,8 @@ _openshift_infra_f5-router()
19721 19721
     local_nonpersistent_flags+=("--f5-private-key=")
19722 19722
     flags+=("--f5-username=")
19723 19723
     local_nonpersistent_flags+=("--f5-username=")
19724
+    flags+=("--f5-vxlan-gateway-cidr=")
19725
+    local_nonpersistent_flags+=("--f5-vxlan-gateway-cidr=")
19724 19726
     flags+=("--fields=")
19725 19727
     local_nonpersistent_flags+=("--fields=")
19726 19728
     flags+=("--hostname-template=")
... ...
@@ -19874,6 +19874,8 @@ _openshift_infra_f5-router()
19874 19874
     local_nonpersistent_flags+=("--f5-https-vserver=")
19875 19875
     flags+=("--f5-insecure")
19876 19876
     local_nonpersistent_flags+=("--f5-insecure")
19877
+    flags+=("--f5-internal-address=")
19878
+    local_nonpersistent_flags+=("--f5-internal-address=")
19877 19879
     flags+=("--f5-partition-path=")
19878 19880
     local_nonpersistent_flags+=("--f5-partition-path=")
19879 19881
     flags+=("--f5-password=")
... ...
@@ -19882,6 +19884,8 @@ _openshift_infra_f5-router()
19882 19882
     local_nonpersistent_flags+=("--f5-private-key=")
19883 19883
     flags+=("--f5-username=")
19884 19884
     local_nonpersistent_flags+=("--f5-username=")
19885
+    flags+=("--f5-vxlan-gateway-cidr=")
19886
+    local_nonpersistent_flags+=("--f5-vxlan-gateway-cidr=")
19885 19887
     flags+=("--fields=")
19886 19888
     local_nonpersistent_flags+=("--fields=")
19887 19889
     flags+=("--hostname-template=")
... ...
@@ -72,6 +72,10 @@ You may restrict the set of routes exposed to a single project (with \-\-namespa
72 72
     Skip strict certificate verification
73 73
 
74 74
 .PP
75
+\fB\-\-f5\-internal\-address\fP=""
76
+    The F5 BIG\-IP internal interface's IP address
77
+
78
+.PP
75 79
 \fB\-\-f5\-partition\-path\fP="/Common"
76 80
     The F5 BIG\-IP partition path to use
77 81
 
... ...
@@ -88,6 +92,10 @@ You may restrict the set of routes exposed to a single project (with \-\-namespa
88 88
     The username for F5 BIG\-IP's management utility
89 89
 
90 90
 .PP
91
+\fB\-\-f5\-vxlan\-gateway\-cidr\fP=""
92
+    The F5 BIG\-IP gateway\-ip\-address/cidr\-mask for setting up the VxLAN
93
+
94
+.PP
91 95
 \fB\-\-fields\fP=""
92 96
     A field selector to apply to routes to watch
93 97
 
... ...
@@ -76,6 +76,17 @@ type F5Router struct {
76 76
 	// normally used to create access control boundaries for users
77 77
 	// and applications.
78 78
 	PartitionPath string
79
+
80
+	// VxlanGateway is the ip address assigned to the local tunnel interface
81
+	// inside F5 box. This address is the one that the packets generated from F5
82
+	// will carry. The pods will return the packets to this address itself.
83
+	// It is important that the gateway be one of the ip addresses of the subnet
84
+	// that has been generated for F5.
85
+	VxlanGateway string
86
+
87
+	// InternalAddress is the ip address of the vtep interface used to connect to
88
+	// VxLAN overlay. It is the hostIP address listed in the subnet generated for F5
89
+	InternalAddress string
79 90
 }
80 91
 
81 92
 // Bind binds F5Router arguments to flags
... ...
@@ -89,6 +100,8 @@ func (o *F5Router) Bind(flag *pflag.FlagSet) {
89 89
 	flag.StringVar(&o.PrivateKey, "f5-private-key", util.Env("ROUTER_EXTERNAL_HOST_PRIVKEY", ""), "The path to the F5 BIG-IP SSH private key file")
90 90
 	flag.BoolVar(&o.Insecure, "f5-insecure", util.Env("ROUTER_EXTERNAL_HOST_INSECURE", "") == "true", "Skip strict certificate verification")
91 91
 	flag.StringVar(&o.PartitionPath, "f5-partition-path", util.Env("ROUTER_EXTERNAL_HOST_PARTITION_PATH", f5plugin.F5DefaultPartitionPath), "The F5 BIG-IP partition path to use")
92
+	flag.StringVar(&o.InternalAddress, "f5-internal-address", util.Env("ROUTER_EXTERNAL_HOST_INTERNAL_ADDRESS", ""), "The F5 BIG-IP internal interface's IP address")
93
+	flag.StringVar(&o.VxlanGateway, "f5-vxlan-gateway-cidr", util.Env("ROUTER_EXTERNAL_HOST_VXLAN_GW_CIDR", ""), "The F5 BIG-IP gateway-ip-address/cidr-mask for setting up the VxLAN")
92 94
 }
93 95
 
94 96
 // Validate verifies the required F5 flags are present
... ...
@@ -109,6 +122,11 @@ func (o *F5Router) Validate() error {
109 109
 		return errors.New("F5 HTTP and HTTPS vservers cannot both be blank")
110 110
 	}
111 111
 
112
+	valid := (len(o.VxlanGateway) == 0 && len(o.InternalAddress) == 0) || (len(o.VxlanGateway) != 0 && len(o.InternalAddress) != 0)
113
+	if !valid {
114
+		return errors.New("For VxLAN setup, both internal-address and gateway-cidr must be specified")
115
+	}
116
+
112 117
 	return nil
113 118
 }
114 119
 
... ...
@@ -158,14 +176,16 @@ func (o *F5RouterOptions) Validate() error {
158 158
 // Run launches an F5 route sync process using the provided options. It never exits.
159 159
 func (o *F5RouterOptions) Run() error {
160 160
 	cfg := f5plugin.F5PluginConfig{
161
-		Host:          o.Host,
162
-		Username:      o.Username,
163
-		Password:      o.Password,
164
-		HttpVserver:   o.HttpVserver,
165
-		HttpsVserver:  o.HttpsVserver,
166
-		PrivateKey:    o.PrivateKey,
167
-		Insecure:      o.Insecure,
168
-		PartitionPath: o.PartitionPath,
161
+		Host:            o.Host,
162
+		Username:        o.Username,
163
+		Password:        o.Password,
164
+		HttpVserver:     o.HttpVserver,
165
+		HttpsVserver:    o.HttpsVserver,
166
+		PrivateKey:      o.PrivateKey,
167
+		Insecure:        o.Insecure,
168
+		PartitionPath:   o.PartitionPath,
169
+		InternalAddress: o.InternalAddress,
170
+		VxlanGateway:    o.VxlanGateway,
169 171
 	}
170 172
 	f5Plugin, err := f5plugin.NewF5Plugin(cfg)
171 173
 	if err != nil {
... ...
@@ -181,7 +201,8 @@ func (o *F5RouterOptions) Run() error {
181 181
 	plugin := controller.NewUniqueHost(statusPlugin, o.RouteSelectionFunc(), statusPlugin)
182 182
 
183 183
 	factory := o.RouterSelection.NewFactory(oc, kc)
184
-	controller := factory.Create(plugin)
184
+	watchNodes := (len(o.InternalAddress) != 0 && len(o.VxlanGateway) != 0)
185
+	controller := factory.Create(plugin, watchNodes)
185 186
 	controller.Run()
186 187
 
187 188
 	select {}
... ...
@@ -210,7 +210,7 @@ func (o *TemplateRouterOptions) Run() error {
210 210
 	plugin := controller.NewUniqueHost(nextPlugin, o.RouteSelectionFunc(), controller.RejectionRecorder(statusPlugin))
211 211
 
212 212
 	factory := o.RouterSelection.NewFactory(oc, kc)
213
-	controller := factory.Create(plugin)
213
+	controller := factory.Create(plugin, false)
214 214
 	controller.Run()
215 215
 
216 216
 	proc.StartReaper()
... ...
@@ -531,6 +531,7 @@ func GetBootstrapClusterRoles() []authorizationapi.ClusterRole {
531 531
 			Rules: []authorizationapi.PolicyRule{
532 532
 				authorizationapi.NewRule("list", "watch").Groups(kapiGroup).Resources("endpoints").RuleOrDie(),
533 533
 				authorizationapi.NewRule("list", "watch").Groups(kapiGroup).Resources("services").RuleOrDie(),
534
+				authorizationapi.NewRule("list", "watch").Groups(kapiGroup).Resources("nodes").RuleOrDie(),
534 535
 
535 536
 				authorizationapi.NewRule("list", "watch").Groups(routeGroup).Resources("routes").RuleOrDie(),
536 537
 				authorizationapi.NewRule("update").Groups(routeGroup).Resources("routes/status").RuleOrDie(),
... ...
@@ -28,6 +28,7 @@ type RouterController struct {
28 28
 
29 29
 	Plugin        router.Plugin
30 30
 	NextRoute     func() (watch.EventType, *routeapi.Route, error)
31
+	NextNode      func() (watch.EventType, *kapi.Node, error)
31 32
 	NextEndpoints func() (watch.EventType, *kapi.Endpoints, error)
32 33
 
33 34
 	RoutesListConsumed    func() bool
... ...
@@ -36,6 +37,8 @@ type RouterController struct {
36 36
 	endpointsListConsumed bool
37 37
 	filteredByNamespace   bool
38 38
 
39
+	WatchNodes bool
40
+
39 41
 	Namespaces            NamespaceLister
40 42
 	NamespaceSyncInterval time.Duration
41 43
 	NamespaceWaitInterval time.Duration
... ...
@@ -51,6 +54,9 @@ func (c *RouterController) Run() {
51 51
 	}
52 52
 	go utilwait.Forever(c.HandleRoute, 0)
53 53
 	go utilwait.Forever(c.HandleEndpoints, 0)
54
+	if c.WatchNodes {
55
+		go utilwait.Forever(c.HandleNode, 0)
56
+	}
54 57
 }
55 58
 
56 59
 func (c *RouterController) HandleNamespaces() {
... ...
@@ -78,6 +84,25 @@ func (c *RouterController) HandleNamespaces() {
78 78
 	glog.V(4).Infof("Unable to update list of namespaces")
79 79
 }
80 80
 
81
+// HandleNode handles a single Node event and synchronizes the router backend
82
+func (c *RouterController) HandleNode() {
83
+	eventType, node, err := c.NextNode()
84
+	if err != nil {
85
+		utilruntime.HandleError(fmt.Errorf("unable to read nodes: %v", err))
86
+		return
87
+	}
88
+
89
+	c.lock.Lock()
90
+	defer c.lock.Unlock()
91
+
92
+	glog.V(4).Infof("Processing Node : %s", node.Name)
93
+	glog.V(4).Infof("           Event: %s", eventType)
94
+
95
+	if err := c.Plugin.HandleNode(eventType, node); err != nil {
96
+		utilruntime.HandleError(err)
97
+	}
98
+}
99
+
81 100
 // HandleRoute handles a single Route event and synchronizes the router backend.
82 101
 func (c *RouterController) HandleRoute() {
83 102
 	eventType, route, err := c.NextRoute()
... ...
@@ -17,6 +17,9 @@ type fakeRouterPlugin struct {
17 17
 func (p *fakeRouterPlugin) HandleRoute(t watch.EventType, route *routeapi.Route) error {
18 18
 	return nil
19 19
 }
20
+func (p *fakeRouterPlugin) HandleNode(t watch.EventType, node *kapi.Node) error {
21
+	return nil
22
+}
20 23
 func (p *fakeRouterPlugin) HandleEndpoints(watch.EventType, *kapi.Endpoints) error {
21 24
 	return nil
22 25
 }
... ...
@@ -46,6 +49,9 @@ func TestRouterController_updateLastSyncProcessed(t *testing.T) {
46 46
 		NextRoute: func() (watch.EventType, *routeapi.Route, error) {
47 47
 			return watch.Modified, &routeapi.Route{}, nil
48 48
 		},
49
+		NextNode: func() (watch.EventType, *kapi.Node, error) {
50
+			return watch.Modified, &kapi.Node{}, nil
51
+		},
49 52
 		EndpointsListConsumed: func() bool {
50 53
 			return true
51 54
 		},
... ...
@@ -38,6 +38,11 @@ func NewExtendedValidator(plugin router.Plugin, recorder RejectionRecorder) *Ext
38 38
 	}
39 39
 }
40 40
 
41
+// HandleNode processes watch events on the node resource
42
+func (p *ExtendedValidator) HandleNode(eventType watch.EventType, node *kapi.Node) error {
43
+	return p.plugin.HandleNode(eventType, node)
44
+}
45
+
41 46
 // HandleEndpoints processes watch events on the Endpoints resource.
42 47
 func (p *ExtendedValidator) HandleEndpoints(eventType watch.EventType, endpoints *kapi.Endpoints) error {
43 48
 	return p.plugin.HandleEndpoints(eventType, endpoints)
... ...
@@ -27,6 +27,7 @@ import (
27 27
 type RouterControllerFactory struct {
28 28
 	KClient        kclient.EndpointsNamespacer
29 29
 	OSClient       osclient.RoutesNamespacer
30
+	NodeClient     kclient.NodesInterface
30 31
 	Namespaces     controller.NamespaceLister
31 32
 	ResyncInterval time.Duration
32 33
 	Namespace      string
... ...
@@ -35,10 +36,11 @@ type RouterControllerFactory struct {
35 35
 }
36 36
 
37 37
 // NewDefaultRouterControllerFactory initializes a default router controller factory.
38
-func NewDefaultRouterControllerFactory(oc osclient.RoutesNamespacer, kc kclient.EndpointsNamespacer) *RouterControllerFactory {
38
+func NewDefaultRouterControllerFactory(oc osclient.RoutesNamespacer, kc kclient.Interface) *RouterControllerFactory {
39 39
 	return &RouterControllerFactory{
40 40
 		KClient:        kc,
41 41
 		OSClient:       oc,
42
+		NodeClient:     kc,
42 43
 		ResyncInterval: 10 * time.Minute,
43 44
 
44 45
 		Namespace: kapi.NamespaceAll,
... ...
@@ -49,7 +51,7 @@ func NewDefaultRouterControllerFactory(oc osclient.RoutesNamespacer, kc kclient.
49 49
 
50 50
 // Create begins listing and watching against the API server for the desired route and endpoint
51 51
 // resources. It spawns child goroutines that cannot be terminated.
52
-func (factory *RouterControllerFactory) Create(plugin router.Plugin) *controller.RouterController {
52
+func (factory *RouterControllerFactory) Create(plugin router.Plugin, watchNodes bool) *controller.RouterController {
53 53
 	routeEventQueue := oscache.NewEventQueue(cache.MetaNamespaceKeyFunc)
54 54
 	cache.NewReflector(&routeLW{
55 55
 		client:    factory.OSClient,
... ...
@@ -65,6 +67,15 @@ func (factory *RouterControllerFactory) Create(plugin router.Plugin) *controller
65 65
 		// we do not scope endpoints by labels or fields because the route labels != endpoints labels
66 66
 	}, &kapi.Endpoints{}, endpointsEventQueue, factory.ResyncInterval).Run()
67 67
 
68
+	nodeEventQueue := oscache.NewEventQueue(cache.MetaNamespaceKeyFunc)
69
+	if watchNodes {
70
+		cache.NewReflector(&nodeLW{
71
+			client: factory.NodeClient,
72
+			field:  fields.Everything(),
73
+			label:  labels.Everything(),
74
+		}, &kapi.Node{}, nodeEventQueue, factory.ResyncInterval).Run()
75
+	}
76
+
68 77
 	return &controller.RouterController{
69 78
 		Plugin: plugin,
70 79
 		NextEndpoints: func() (watch.EventType, *kapi.Endpoints, error) {
... ...
@@ -81,6 +92,13 @@ func (factory *RouterControllerFactory) Create(plugin router.Plugin) *controller
81 81
 			}
82 82
 			return eventType, obj.(*routeapi.Route), nil
83 83
 		},
84
+		NextNode: func() (watch.EventType, *kapi.Node, error) {
85
+			eventType, obj, err := nodeEventQueue.Pop()
86
+			if err != nil {
87
+				return watch.Error, nil, err
88
+			}
89
+			return eventType, obj.(*kapi.Node), nil
90
+		},
84 91
 		EndpointsListConsumed: func() bool {
85 92
 			return endpointsEventQueue.ListConsumed()
86 93
 		},
... ...
@@ -94,6 +112,7 @@ func (factory *RouterControllerFactory) Create(plugin router.Plugin) *controller
94 94
 		NamespaceSyncInterval: factory.ResyncInterval - 10*time.Second,
95 95
 		NamespaceWaitInterval: 10 * time.Second,
96 96
 		NamespaceRetries:      5,
97
+		WatchNodes:            watchNodes,
97 98
 	}
98 99
 }
99 100
 
... ...
@@ -256,3 +275,23 @@ func (lw *endpointsLW) Watch(options kapi.ListOptions) (watch.Interface, error)
256 256
 	}
257 257
 	return lw.client.Endpoints(lw.namespace).Watch(opts)
258 258
 }
259
+
260
+// nodeLW is a list watcher for nodes.
261
+type nodeLW struct {
262
+	client kclient.NodesInterface
263
+	label  labels.Selector
264
+	field  fields.Selector
265
+}
266
+
267
+func (lw *nodeLW) List(options kapi.ListOptions) (runtime.Object, error) {
268
+	return lw.client.Nodes().List(options)
269
+}
270
+
271
+func (lw *nodeLW) Watch(options kapi.ListOptions) (watch.Interface, error) {
272
+	opts := kapi.ListOptions{
273
+		LabelSelector:   lw.label,
274
+		FieldSelector:   lw.field,
275
+		ResourceVersion: options.ResourceVersion,
276
+	}
277
+	return lw.client.Nodes().Watch(opts)
278
+}
... ...
@@ -295,6 +295,10 @@ func (a *StatusAdmitter) HandleRoute(eventType watch.EventType, route *routeapi.
295 295
 	return a.plugin.HandleRoute(eventType, route)
296 296
 }
297 297
 
298
+func (a *StatusAdmitter) HandleNode(eventType watch.EventType, node *kapi.Node) error {
299
+	return a.plugin.HandleNode(eventType, node)
300
+}
301
+
298 302
 func (a *StatusAdmitter) HandleEndpoints(eventType watch.EventType, route *kapi.Endpoints) error {
299 303
 	return a.plugin.HandleEndpoints(eventType, route)
300 304
 }
... ...
@@ -28,6 +28,11 @@ func (p *fakePlugin) HandleRoute(t watch.EventType, route *routeapi.Route) error
28 28
 	p.t, p.route = t, route
29 29
 	return p.err
30 30
 }
31
+
32
+func (p *fakePlugin) HandleNode(t watch.EventType, node *kapi.Node) error {
33
+	return fmt.Errorf("not expected")
34
+}
35
+
31 36
 func (p *fakePlugin) HandleEndpoints(watch.EventType, *kapi.Endpoints) error {
32 37
 	return fmt.Errorf("not expected")
33 38
 }
... ...
@@ -84,6 +84,11 @@ func (p *UniqueHost) HandleEndpoints(eventType watch.EventType, endpoints *kapi.
84 84
 	return p.plugin.HandleEndpoints(eventType, endpoints)
85 85
 }
86 86
 
87
+// HandleNode processes watch events on the Node resource and calls the router
88
+func (p *UniqueHost) HandleNode(eventType watch.EventType, node *kapi.Node) error {
89
+	return p.plugin.HandleNode(eventType, node)
90
+}
91
+
87 92
 // HandleRoute processes watch events on the Route resource.
88 93
 // TODO: this function can probably be collapsed with the router itself, as a function that
89 94
 //   determines which component needs to be recalculated (which template) and then does so
... ...
@@ -7,6 +7,7 @@ import (
7 7
 	"fmt"
8 8
 	"io"
9 9
 	"io/ioutil"
10
+	"net"
10 11
 	"net/http"
11 12
 	"os"
12 13
 	"os/exec"
... ...
@@ -21,6 +22,9 @@ import (
21 21
 const (
22 22
 	// Default F5 partition path to use for syncing route config.
23 23
 	F5DefaultPartitionPath = "/Common"
24
+	F5VxLANTunnelName      = "vxlan5000"
25
+	F5VxLANProfileName     = "vxlan-ose"
26
+	HTTP_CONFLICT_CODE     = 409
24 27
 )
25 28
 
26 29
 // Error implements the error interface.
... ...
@@ -104,6 +108,21 @@ type f5LTMCfg struct {
104 104
 	// are normally used to create an access control boundary for
105 105
 	// F5 users and applications.
106 106
 	partitionPath string
107
+
108
+	// vxlanGateway is the ip address assigned to the local tunnel interface
109
+	// inside F5 box. This address is the one that the packets generated from F5
110
+	// will carry. The pods will return the packets to this address itself.
111
+	// It is important that the gateway be one of the ip addresses of the subnet
112
+	// that has been generated for F5.
113
+	vxlanGateway string
114
+
115
+	// internalAddress is the ip address of the vtep interface used to connect to
116
+	// VxLAN overlay. It is the hostIP address listed in the subnet generated for F5
117
+	internalAddress string
118
+
119
+	// setupOSDNVxLAN is the boolean that conveys if F5 needs to setup a VxLAN
120
+	// to hook up with openshift-sdn
121
+	setupOSDNVxLAN bool
107 122
 }
108 123
 
109 124
 const (
... ...
@@ -327,17 +346,21 @@ func newF5LTM(cfg f5LTMCfg) (*f5LTM, error) {
327 327
 
328 328
 	// Ensure path is rooted.
329 329
 	partitionPath = path.Join("/", partitionPath)
330
+	setupOSDNVxLAN := (len(cfg.vxlanGateway) != 0 && len(cfg.internalAddress) != 0)
330 331
 
331 332
 	router := &f5LTM{
332 333
 		f5LTMCfg: f5LTMCfg{
333
-			host:          cfg.host,
334
-			username:      cfg.username,
335
-			password:      cfg.password,
336
-			httpVserver:   cfg.httpVserver,
337
-			httpsVserver:  cfg.httpsVserver,
338
-			privkey:       privkeyFileName,
339
-			insecure:      cfg.insecure,
340
-			partitionPath: partitionPath,
334
+			host:            cfg.host,
335
+			username:        cfg.username,
336
+			password:        cfg.password,
337
+			httpVserver:     cfg.httpVserver,
338
+			httpsVserver:    cfg.httpsVserver,
339
+			privkey:         privkeyFileName,
340
+			insecure:        cfg.insecure,
341
+			partitionPath:   partitionPath,
342
+			vxlanGateway:    cfg.vxlanGateway,
343
+			internalAddress: cfg.internalAddress,
344
+			setupOSDNVxLAN:  setupOSDNVxLAN,
341 345
 		},
342 346
 		poolMembers: map[string]map[string]bool{},
343 347
 		routes:      map[string]map[string]bool{},
... ...
@@ -395,6 +418,7 @@ func (f5 *f5LTM) restRequest(verb string, url string, payload io.Reader,
395 395
 
396 396
 	client := &http.Client{Transport: tr}
397 397
 
398
+	glog.V(4).Infof("Request sent: %v\n", req)
398 399
 	resp, err := client.Do(req)
399 400
 	if err != nil {
400 401
 		errorResult.err = fmt.Errorf("client.Do failed: %v", err)
... ...
@@ -461,6 +485,68 @@ func (f5 *f5LTM) delete(url string, result interface{}) error {
461 461
 // Routines for controlling F5.
462 462
 //
463 463
 
464
+// ensureVxLANTunnel sets up the VxLAN tunnel profile and tunnel+selfIP
465
+func (f5 *f5LTM) ensureVxLANTunnel() error {
466
+	glog.V(4).Infof("Checking and installing VxLAN setup")
467
+
468
+	// create the profile
469
+	url := fmt.Sprintf("https://%s/mgmt/tm/net/tunnels/vxlan", f5.host)
470
+	profilePayload := f5CreateVxLANProfilePayload{
471
+		Name:         F5VxLANProfileName,
472
+		Partition:    f5.partitionPath,
473
+		FloodingType: "multipoint",
474
+		Port:         4789,
475
+	}
476
+	err := f5.post(url, profilePayload, nil)
477
+	if err != nil && err.(F5Error).httpStatusCode != HTTP_CONFLICT_CODE {
478
+		// error HTTP_CONFLICT_CODE is fine, it just means the tunnel profile already exists
479
+		glog.V(4).Infof("Error while creating vxlan tunnel - %v", err)
480
+		return err
481
+	}
482
+
483
+	// create the tunnel
484
+	url = fmt.Sprintf("https://%s/mgmt/tm/net/tunnels/tunnel", f5.host)
485
+	tunnelPayload := f5CreateVxLANTunnelPayload{
486
+		Name:         F5VxLANTunnelName,
487
+		Partition:    f5.partitionPath,
488
+		Key:          0,
489
+		LocalAddress: f5.internalAddress,
490
+		Mode:         "bidirectional",
491
+		Mtu:          "0",
492
+		Profile:      path.Join(f5.partitionPath, F5VxLANProfileName),
493
+		Tos:          "preserve",
494
+		Transparent:  "disabled",
495
+		UsePmtu:      "enabled",
496
+	}
497
+	err = f5.post(url, tunnelPayload, nil)
498
+	if err != nil && err.(F5Error).httpStatusCode != HTTP_CONFLICT_CODE {
499
+		// error HTTP_CONFLICT_CODE is fine, it just means the tunnel already exists
500
+		return err
501
+	}
502
+
503
+	selfUrl := fmt.Sprintf("https://%s/mgmt/tm/net/self", f5.host)
504
+	netSelfPayload := f5CreateNetSelfPayload{
505
+		Name:                  f5.vxlanGateway,
506
+		Partition:             f5.partitionPath,
507
+		Address:               f5.vxlanGateway,
508
+		AddressSource:         "from-user",
509
+		Floating:              "disabled",
510
+		InheritedTrafficGroup: "false",
511
+		TrafficGroup:          path.Join(f5.partitionPath, "traffic-group-local-only"),
512
+		Unit:                  0,
513
+		Vlan:                  path.Join(f5.partitionPath, F5VxLANTunnelName),
514
+		AllowService:          "all",
515
+	}
516
+	// create the net self IP
517
+	err = f5.post(selfUrl, netSelfPayload, nil)
518
+	if err != nil && err.(F5Error).httpStatusCode != HTTP_CONFLICT_CODE {
519
+		// error HTTP_CONFLICT_CODE is ok, netSelf already exists
520
+		return err
521
+	}
522
+
523
+	return nil
524
+}
525
+
464 526
 // ensurePolicyExists checks whether the specified policy exists and creates it
465 527
 // if not.
466 528
 func (f5 *f5LTM) ensurePolicyExists(policyName string) error {
... ...
@@ -484,14 +570,28 @@ func (f5 *f5LTM) ensurePolicyExists(policyName string) error {
484 484
 
485 485
 	policiesUrl := fmt.Sprintf("https://%s/mgmt/tm/ltm/policy", f5.host)
486 486
 
487
-	policyPayload := f5Policy{
488
-		Name:     policyName,
489
-		Controls: []string{"forwarding"},
490
-		Requires: []string{"http"},
491
-		Strategy: "best-match",
487
+	if f5.setupOSDNVxLAN {
488
+		// if vxlan needs to be setup, it will only happen
489
+		// with ver12, for which we need to use a different payload
490
+		policyPayload := f5Ver12Policy{
491
+			Name:        policyName,
492
+			TmPartition: f5.partitionPath,
493
+			Controls:    []string{"forwarding"},
494
+			Requires:    []string{"http"},
495
+			Strategy:    "best-match",
496
+			Legacy:      true,
497
+		}
498
+		err = f5.post(policiesUrl, policyPayload, nil)
499
+	} else {
500
+		policyPayload := f5Policy{
501
+			Name:     policyName,
502
+			Controls: []string{"forwarding"},
503
+			Requires: []string{"http"},
504
+			Strategy: "best-match",
505
+		}
506
+		err = f5.post(policiesUrl, policyPayload, nil)
492 507
 	}
493 508
 
494
-	err = f5.post(policiesUrl, policyPayload, nil)
495 509
 	if err != nil {
496 510
 		return err
497 511
 	}
... ...
@@ -717,7 +817,7 @@ func (f5 *f5LTM) addPartitionPath(pathName string) (bool, error) {
717 717
 	payload := f5AddPartitionPathPayload{Name: pathName}
718 718
 	err := f5.post(uri, payload, nil)
719 719
 	if err != nil {
720
-		if err.(F5Error).httpStatusCode != 409 {
720
+		if err.(F5Error).httpStatusCode != HTTP_CONFLICT_CODE {
721 721
 			glog.Errorf("Error adding partition path %q error: %v", pathName, err)
722 722
 			return false, err
723 723
 		}
... ...
@@ -828,11 +928,77 @@ func (f5 *f5LTM) Initialize() error {
828 828
 		}
829 829
 	}
830 830
 
831
+	if f5.setupOSDNVxLAN {
832
+		err = f5.ensureVxLANTunnel()
833
+		if err != nil {
834
+			return err
835
+		}
836
+	}
837
+
831 838
 	glog.V(4).Infof("F5 initialization is complete.")
832 839
 
833 840
 	return nil
834 841
 }
835 842
 
843
+func checkIPAndGetMac(ipStr string) (string, error) {
844
+	ip := net.ParseIP(ipStr)
845
+	if ip == nil {
846
+		errStr := fmt.Sprintf("vtep IP '%s' is not a valid IP address", ipStr)
847
+		glog.Warning(errStr)
848
+		return "", fmt.Errorf(errStr)
849
+	}
850
+	ip4 := ip.To4()
851
+	if ip4 == nil {
852
+		errStr := fmt.Sprintf("vtep IP '%s' is not a valid IPv4 address", ipStr)
853
+		glog.Warning(errStr)
854
+		return "", fmt.Errorf(errStr)
855
+	}
856
+	macAddr := fmt.Sprintf("0a:0a:%02x:%02x:%02x:%02x", ip4[0], ip4[1], ip4[2], ip4[3])
857
+	return macAddr, nil
858
+}
859
+
860
+// AddVtep adds the Vtep IP to the VxLAN device's FDB
861
+func (f5 *f5LTM) AddVtep(ipStr string) error {
862
+	if !f5.setupOSDNVxLAN {
863
+		return nil
864
+	}
865
+	macAddr, err := checkIPAndGetMac(ipStr)
866
+	if err != nil {
867
+		return err
868
+	}
869
+
870
+	err = f5.ensurePartitionPathExists(f5.partitionPath)
871
+	if err != nil {
872
+		return err
873
+	}
874
+
875
+	url := fmt.Sprintf("https://%s/mgmt/tm/net/fdb/tunnel/%s~%s/records", f5.host, strings.Replace(f5.partitionPath, "/", "~", -1), F5VxLANTunnelName)
876
+	payload := f5AddFDBRecordPayload{
877
+		Name:     macAddr,
878
+		Endpoint: ipStr,
879
+	}
880
+	return f5.post(url, payload, nil)
881
+}
882
+
883
+// RemoveVtep removes the Vtep IP from the VxLAN device's FDB
884
+func (f5 *f5LTM) RemoveVtep(ipStr string) error {
885
+	if !f5.setupOSDNVxLAN {
886
+		return nil
887
+	}
888
+	macAddr, err := checkIPAndGetMac(ipStr)
889
+	if err != nil {
890
+		return err
891
+	}
892
+
893
+	err = f5.ensurePartitionPathExists(f5.partitionPath)
894
+	if err != nil {
895
+		return err
896
+	}
897
+
898
+	url := fmt.Sprintf("https://%s/mgmt/tm/net/fdb/tunnel/%s~%s/records/%s", f5.host, strings.Replace(f5.partitionPath, "/", "~", -1), F5VxLANTunnelName, macAddr)
899
+	return f5.delete(url, nil)
900
+}
901
+
836 902
 // CreatePool creates a pool named poolname on F5 BIG-IP.
837 903
 func (f5 *f5LTM) CreatePool(poolname string) error {
838 904
 	url := fmt.Sprintf("https://%s/mgmt/tm/ltm/pool", f5.host)
... ...
@@ -1095,7 +1261,7 @@ func (f5 *f5LTM) addRoute(policyname, routename, poolname, hostname,
1095 1095
 
1096 1096
 	err := f5.post(rulesUrl, rulesPayload, nil)
1097 1097
 	if err != nil {
1098
-		if err.(F5Error).httpStatusCode == 409 {
1098
+		if err.(F5Error).httpStatusCode == HTTP_CONFLICT_CODE {
1099 1099
 			glog.V(4).Infof("Warning: Rule %s already exists; continuing with"+
1100 1100
 				" initialization in case the existing rule is only partially"+
1101 1101
 				" initialized...", routename)
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"k8s.io/kubernetes/pkg/watch"
10 10
 
11 11
 	routeapi "github.com/openshift/origin/pkg/route/api"
12
+	"github.com/openshift/origin/pkg/util/netutils"
12 13
 )
13 14
 
14 15
 // F5Plugin holds state for the f5 plugin.
... ...
@@ -52,19 +53,32 @@ type F5PluginConfig struct {
52 52
 	// PartitionPath specifies the F5 partition path to use. This is used
53 53
 	// to create an access control boundary for users and applications.
54 54
 	PartitionPath string
55
+
56
+	// VxlanGateway is the ip address assigned to the local tunnel interface
57
+	// inside F5 box. This address is the one that the packets generated from F5
58
+	// will carry. The pods will return the packets to this address itself.
59
+	// It is important that the gateway be one of the ip addresses of the subnet
60
+	// that has been generated for F5.
61
+	VxlanGateway string
62
+
63
+	// InternalAddress is the ip address of the vtep interface used to connect to
64
+	// VxLAN overlay. It is the hostIP address listed in the subnet generated for F5
65
+	InternalAddress string
55 66
 }
56 67
 
57 68
 // NewF5Plugin makes a new f5 router plugin.
58 69
 func NewF5Plugin(cfg F5PluginConfig) (*F5Plugin, error) {
59 70
 	f5LTMCfg := f5LTMCfg{
60
-		host:          cfg.Host,
61
-		username:      cfg.Username,
62
-		password:      cfg.Password,
63
-		httpVserver:   cfg.HttpVserver,
64
-		httpsVserver:  cfg.HttpsVserver,
65
-		privkey:       cfg.PrivateKey,
66
-		insecure:      cfg.Insecure,
67
-		partitionPath: cfg.PartitionPath,
71
+		host:            cfg.Host,
72
+		username:        cfg.Username,
73
+		password:        cfg.Password,
74
+		httpVserver:     cfg.HttpVserver,
75
+		httpsVserver:    cfg.HttpsVserver,
76
+		privkey:         cfg.PrivateKey,
77
+		insecure:        cfg.Insecure,
78
+		partitionPath:   cfg.PartitionPath,
79
+		vxlanGateway:    cfg.VxlanGateway,
80
+		internalAddress: cfg.InternalAddress,
68 81
 	}
69 82
 	f5, err := newF5LTM(f5LTMCfg)
70 83
 	if err != nil {
... ...
@@ -466,10 +480,54 @@ func (p *F5Plugin) deleteRoute(routename string) error {
466 466
 	return nil
467 467
 }
468 468
 
469
+func getNodeIP(node *kapi.Node) (string, error) {
470
+	if len(node.Status.Addresses) > 0 && node.Status.Addresses[0].Address != "" {
471
+		return node.Status.Addresses[0].Address, nil
472
+	} else {
473
+		return netutils.GetNodeIP(node.Name)
474
+	}
475
+}
476
+
469 477
 func (p *F5Plugin) HandleNamespaces(namespaces sets.String) error {
470 478
 	return fmt.Errorf("namespace limiting for F5 is not implemented")
471 479
 }
472 480
 
481
+func (p *F5Plugin) HandleNode(eventType watch.EventType, node *kapi.Node) error {
482
+	// The F5 appliance, if hooked to use the VxLAN encapsulation
483
+	// should have its FDB updated depending on nodes arriving and leaving the cluster
484
+	switch eventType {
485
+	case watch.Added:
486
+		// New VTEP created, add the record to the vxlan fdb
487
+		ip, err := getNodeIP(node)
488
+		if err != nil {
489
+			// just log the error
490
+			glog.Warningf("Error in obtaining IP address of newly added node %s - %v", node.Name, err)
491
+			return nil
492
+		}
493
+		err = p.F5Client.AddVtep(ip)
494
+		if err != nil {
495
+			glog.Errorf("Error in adding node '%s' to F5s FDB - %v", ip, err)
496
+			return err
497
+		}
498
+	case watch.Deleted:
499
+		// VTEP deleted, delete the record from vxlan fdb
500
+		ip, err := getNodeIP(node)
501
+		if err != nil {
502
+			// just log the error
503
+			glog.Warningf("Error in obtaining IP address of deleted node %s - %v", node.Name, err)
504
+			return nil
505
+		}
506
+		err = p.F5Client.RemoveVtep(ip)
507
+		if err != nil {
508
+			glog.Errorf("Error in removing node '%s' from F5s FDB - %v", ip, err)
509
+			return err
510
+		}
511
+	case watch.Modified:
512
+		// ignore the modified event. Change in IP address of the node is not supported.
513
+	}
514
+	return nil
515
+}
516
+
473 517
 // HandleRoute processes watch events on the Route resource and
474 518
 // creates and deletes policy rules in response.
475 519
 func (p *F5Plugin) HandleRoute(eventType watch.EventType,
... ...
@@ -100,6 +100,37 @@ type f5PoolMemberset struct {
100 100
 	Members []f5PoolMember `json:"items"`
101 101
 }
102 102
 
103
+// f5Ver12Policy represents an F5 BIG-IP LTM policy for versions 12.x
104
+// It describes the payload for a POST request by which the router creates a new policy.
105
+type f5Ver12Policy struct {
106
+	// Name is the name of the policy.
107
+	Name string `json:"name"`
108
+
109
+	// TmPartition is the partition name for the policy
110
+	TmPartition string `json:"tmPartition"`
111
+
112
+	// Controls is a list of F5 BIG-IP LTM features enabled for the pool.
113
+	// Typically we use just forwarding; other possible values are caching,
114
+	// classification, compression, request-adaption, response-adaption, and
115
+	// server-ssl.
116
+	Controls []string `json:"controls"`
117
+
118
+	// Requires is a list of available profile types.  Typically we use just http;
119
+	// other possible values are client-ssl, ssl-persistence, and tcp.
120
+	Requires []string `json:"requires"`
121
+
122
+	// Strategy is the strategy according to which rules are applied to incoming
123
+	// connections when more than one rule matches.  Typically we use best-match;
124
+	// other possible values are all-match and first-match.
125
+	Strategy string `json:"strategy"`
126
+
127
+	// Legacy is the boolean keyword by which ver12.1 can be programmed
128
+	// for creating a policy using this payload. Eventually we need to move
129
+	// to creating Draft policies and then associating them with the virtual servers
130
+	// Note that this keyword will only work with versions 12.1 and above
131
+	Legacy bool `json:"legacy"`
132
+}
133
+
103 134
 // f5Policy represents an F5 BIG-IP LTM policy.  It describes the payload for
104 135
 // a POST request by which the F5 router creates a new policy.
105 136
 type f5Policy struct {
... ...
@@ -293,3 +324,46 @@ type f5AddPartitionPathPayload struct {
293 293
 	// Name is the partition path to be added.
294 294
 	Name string `json:"name"`
295 295
 }
296
+
297
+// Method:POST URL:/mgmt/tm/net/tunnels/vxlan
298
+type f5CreateVxLANProfilePayload struct {
299
+	Name         string `json:"name"`         // <vxlan-profile-name> e.g. vxlan-ose
300
+	Partition    string `json:"partition"`    // /Common
301
+	FloodingType string `json:"floodingType"` // multipoint
302
+	Port         int    `json:"port"`         // 4789 (nothing else will work)
303
+}
304
+
305
+// Method:POST URL:/mgmt/tm/net/tunnels/tunnel
306
+type f5CreateVxLANTunnelPayload struct {
307
+	Name         string `json:"name"`         // vxlan5000
308
+	Partition    string `json:"partition"`    // /Common
309
+	Key          uint32 `json:"key"`          // 0
310
+	LocalAddress string `json:"localAddress"` // 172.30.1.5
311
+	Mode         string `json:"mode"`         // bidirectional
312
+	Mtu          string `json:"mtu"`          // 0
313
+	Profile      string `json:"profile"`      // <partition>/<vxlan-profile-name>
314
+	Tos          string `json:"tos"`          // preserve
315
+	Transparent  string `json:"transparent"`  // disabled
316
+	UsePmtu      string `json:"usePmtu"`      // enabled
317
+}
318
+
319
+// tmsh create net self <local-overlay-address>/<prefix> vlan vxlan5000
320
+// Method: POST URL: /mgmt/tm/net/self
321
+type f5CreateNetSelfPayload struct {
322
+	Name                  string `json:"name"`                  // “10.0.1.10/16",
323
+	Partition             string `json:"partition"`             // "Common",
324
+	Address               string `json:"address"`               // “10.0.1.10/16",
325
+	AddressSource         string `json:"addressSource"`         // "from-user",
326
+	Floating              string `json:"floating"`              // "disabled",
327
+	InheritedTrafficGroup string `json:"inheritedTrafficGroup"` // "false",
328
+	TrafficGroup          string `json:"trafficGroup"`          // "/Common/traffic-group-local-only",
329
+	Unit                  uint32 `json:"unit"`                  // 0,
330
+	Vlan                  string `json:"vlan"`                  // "/Common/vxlan5000",
331
+	AllowService          string `json:"allowService"`          // "all"
332
+}
333
+
334
+// POST /mgmt/tm/net/fdb/tunnel/~Common~vxlan5000/records
335
+type f5AddFDBRecordPayload struct {
336
+	Name     string `json:"name"`     // "02:50:56:c0:00:06",
337
+	Endpoint string `json:"endpoint"` // "10.139.1.1"
338
+}
... ...
@@ -15,5 +15,6 @@ type Plugin interface {
15 15
 	HandleEndpoints(watch.EventType, *kapi.Endpoints) error
16 16
 	// If sent, filter the list of accepted routes and endpoints to this set
17 17
 	HandleNamespaces(namespaces sets.String) error
18
+	HandleNode(watch.EventType, *kapi.Node) error
18 19
 	SetLastSyncProcessed(processed bool) error
19 20
 }
... ...
@@ -176,6 +176,13 @@ func (p *TemplatePlugin) HandleEndpoints(eventType watch.EventType, endpoints *k
176 176
 	return nil
177 177
 }
178 178
 
179
+// HandleNode processes watch events on the Node resource
180
+// The template type of plugin currently does not need to act on such events
181
+// so the implementation just returns without error
182
+func (p *TemplatePlugin) HandleNode(eventType watch.EventType, node *kapi.Node) error {
183
+	return nil
184
+}
185
+
179 186
 // HandleRoute processes watch events on the Route resource.
180 187
 // TODO: this function can probably be collapsed with the router itself, as a function that
181 188
 //   determines which component needs to be recalculated (which template) and then does so
... ...
@@ -23,7 +23,9 @@ var _ = g.Describe("[networking][router] openshift routers", func() {
23 23
 
24 24
 	g.BeforeEach(func() {
25 25
 		// defer oc.Run("delete").Args("-f", configPath).Execute()
26
-		err := oc.Run("create").Args("-f", configPath).Execute()
26
+		err := oc.AsAdmin().Run("policy").Args("add-role-to-user", "system:router", oc.Username()).Execute()
27
+		o.Expect(err).NotTo(o.HaveOccurred())
28
+		err = oc.Run("create").Args("-f", configPath).Execute()
27 29
 		o.Expect(err).NotTo(o.HaveOccurred())
28 30
 	})
29 31
 
... ...
@@ -27,7 +27,9 @@ var _ = g.Describe("[networking][router] weighted openshift router", func() {
27 27
 
28 28
 	g.BeforeEach(func() {
29 29
 		// defer oc.Run("delete").Args("-f", configPath).Execute()
30
-		err := oc.Run("create").Args("-f", configPath).Execute()
30
+		err := oc.AsAdmin().Run("policy").Args("add-role-to-user", "system:router", oc.Username()).Execute()
31
+		o.Expect(err).NotTo(o.HaveOccurred())
32
+		err = oc.Run("create").Args("-f", configPath).Execute()
31 33
 		o.Expect(err).NotTo(o.HaveOccurred())
32 34
 	})
33 35
 
... ...
@@ -36,6 +36,8 @@ func GetDefaultLocalAddress() string {
36 36
 func NewTestHttpService() *TestHttpService {
37 37
 	endpointChannel := make(chan string)
38 38
 	routeChannel := make(chan string)
39
+	nodeChannel := make(chan string)
40
+	svcChannel := make(chan string)
39 41
 
40 42
 	addr := GetDefaultLocalAddress()
41 43
 
... ...
@@ -55,6 +57,8 @@ func NewTestHttpService() *TestHttpService {
55 55
 		PodHttpsCaCert:       []byte(ExampleCACert),
56 56
 		EndpointChannel:      endpointChannel,
57 57
 		RouteChannel:         routeChannel,
58
+		NodeChannel:          nodeChannel,
59
+		SvcChannel:           svcChannel,
58 60
 	}
59 61
 }
60 62
 
... ...
@@ -77,6 +81,8 @@ type TestHttpService struct {
77 77
 	PodTestPath          string
78 78
 	EndpointChannel      chan string
79 79
 	RouteChannel         chan string
80
+	NodeChannel          chan string
81
+	SvcChannel           chan string
80 82
 
81 83
 	listeners []net.Listener
82 84
 }
... ...
@@ -131,6 +137,30 @@ func (s *TestHttpService) handleHelloPodTestSecure(w http.ResponseWriter, r *htt
131 131
 	fmt.Fprint(w, HelloPodPathSecure)
132 132
 }
133 133
 
134
+// handleSvcList handles calls to /api/v1beta1/services and always returns empty data
135
+func (s *TestHttpService) handleSvcList(w http.ResponseWriter, r *http.Request) {
136
+	w.Header().Set("Content-Type", "application/json")
137
+	fmt.Fprint(w, "{}")
138
+}
139
+
140
+// handleSvcWatch handles calls to /api/v1beta1/watch/services and uses the svc channel to simulate watch events
141
+func (s *TestHttpService) handleSvcWatch(w http.ResponseWriter, r *http.Request) {
142
+	w.Header().Set("Content-Type", "application/json")
143
+	io.WriteString(w, <-s.SvcChannel)
144
+}
145
+
146
+// handleNodeList handles calls to /api/v1beta1/nodes and always returns empty data
147
+func (s *TestHttpService) handleNodeList(w http.ResponseWriter, r *http.Request) {
148
+	w.Header().Set("Content-Type", "application/json")
149
+	fmt.Fprint(w, "{}")
150
+}
151
+
152
+// handleNodeWatch handles calls to /api/v1beta1/watch/nodes and uses the node channel to simulate watch events
153
+func (s *TestHttpService) handleNodeWatch(w http.ResponseWriter, r *http.Request) {
154
+	w.Header().Set("Content-Type", "application/json")
155
+	io.WriteString(w, <-s.NodeChannel)
156
+}
157
+
134 158
 // handleRouteWatch handles calls to /osapi/v1beta1/watch/routes and uses the route channel to simulate watch events
135 159
 func (s *TestHttpService) handleRouteWatch(w http.ResponseWriter, r *http.Request) {
136 160
 	w.Header().Set("Content-Type", "application/json")
... ...
@@ -208,6 +238,10 @@ func (s *TestHttpService) startMaster() error {
208 208
 		masterServer.HandleFunc(fmt.Sprintf("/oapi/%s/routes", version), s.handleRouteList)
209 209
 		masterServer.HandleFunc(fmt.Sprintf("/oapi/%s/namespaces/", version), s.handleRouteCalls)
210 210
 		masterServer.HandleFunc(fmt.Sprintf("/oapi/%s/watch/routes", version), s.handleRouteWatch)
211
+		masterServer.HandleFunc(fmt.Sprintf("/api/%s/nodes", version), s.handleNodeList)
212
+		masterServer.HandleFunc(fmt.Sprintf("/api/%s/watch/nodes", version), s.handleNodeWatch)
213
+		masterServer.HandleFunc(fmt.Sprintf("/api/%s/services", version), s.handleSvcList)
214
+		masterServer.HandleFunc(fmt.Sprintf("/api/%s/watch/services", version), s.handleSvcWatch)
211 215
 	}
212 216
 
213 217
 	if err := s.startServing(s.MasterHttpAddr, http.Handler(masterServer)); err != nil {
... ...
@@ -238,6 +238,11 @@ func (p *DelayPlugin) HandleRoute(eventType watch.EventType, route *routeapi.Rou
238 238
 	return p.plugin.HandleRoute(eventType, route)
239 239
 }
240 240
 
241
+func (p *DelayPlugin) HandleNode(eventType watch.EventType, node *kapi.Node) error {
242
+	p.delay()
243
+	return p.plugin.HandleNode(eventType, node)
244
+}
245
+
241 246
 func (p *DelayPlugin) HandleEndpoints(eventType watch.EventType, endpoints *kapi.Endpoints) error {
242 247
 	p.delay()
243 248
 	return p.plugin.HandleEndpoints(eventType, endpoints)
... ...
@@ -277,7 +282,7 @@ func launchRouter(oc osclient.Interface, kc kclient.Interface, maxDelay int32, n
277 277
 	}
278 278
 
279 279
 	factory := controllerfactory.NewDefaultRouterControllerFactory(oc, kc)
280
-	controller := factory.Create(plugin)
280
+	controller := factory.Create(plugin, false)
281 281
 	controller.Run()
282 282
 
283 283
 	return
... ...
@@ -1776,6 +1776,14 @@ items:
1776 1776
     - ""
1777 1777
     attributeRestrictions: null
1778 1778
     resources:
1779
+    - nodes
1780
+    verbs:
1781
+    - list
1782
+    - watch
1783
+  - apiGroups:
1784
+    - ""
1785
+    attributeRestrictions: null
1786
+    resources:
1779 1787
     - routes
1780 1788
     verbs:
1781 1789
     - list