2016-07-28 14:20:26 +00:00
/ *
Copyright 2016 The Kubernetes Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package cloudstack
import (
"fmt"
"strconv"
"github.com/golang/glog"
"github.com/xanzy/go-cloudstack/cloudstack"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/cloudprovider"
)
type loadBalancer struct {
* cloudstack . CloudStackClient
name string
algorithm string
hostIDs [ ] string
ipAddr string
ipAddrID string
networkID string
projectID string
rules map [ string ] * cloudstack . LoadBalancerRule
}
// GetLoadBalancer returns whether the specified load balancer exists, and if so, what its status is.
func ( cs * CSCloud ) GetLoadBalancer ( clusterName string , service * api . Service ) ( * api . LoadBalancerStatus , bool , error ) {
glog . V ( 4 ) . Infof ( "GetLoadBalancer(%v, %v, %v)" , clusterName , service . Namespace , service . Name )
// Get the load balancer details and existing rules.
lb , err := cs . getLoadBalancer ( service )
if err != nil {
return nil , false , err
}
// If we don't have any rules, the load balancer does not exist.
if len ( lb . rules ) == 0 {
return nil , false , nil
}
glog . V ( 4 ) . Infof ( "Found a load balancer associated with IP %v" , lb . ipAddr )
status := & api . LoadBalancerStatus { }
status . Ingress = append ( status . Ingress , api . LoadBalancerIngress { IP : lb . ipAddr } )
return status , true , nil
}
// EnsureLoadBalancer creates a new load balancer, or updates the existing one. Returns the status of the balancer.
func ( cs * CSCloud ) EnsureLoadBalancer ( clusterName string , service * api . Service , hosts [ ] string ) ( status * api . LoadBalancerStatus , err error ) {
glog . V ( 4 ) . Infof ( "EnsureLoadBalancer(%v, %v, %v, %v, %v, %v)" , clusterName , service . Namespace , service . Name , service . Spec . LoadBalancerIP , service . Spec . Ports , hosts )
if len ( service . Spec . Ports ) == 0 {
return nil , fmt . Errorf ( "requested load balancer with no ports" )
}
// Get the load balancer details and existing rules.
lb , err := cs . getLoadBalancer ( service )
if err != nil {
return nil , err
}
// Set the load balancer algorithm.
switch service . Spec . SessionAffinity {
case api . ServiceAffinityNone :
lb . algorithm = "roundrobin"
case api . ServiceAffinityClientIP :
lb . algorithm = "source"
default :
return nil , fmt . Errorf ( "unsupported load balancer affinity: %v" , service . Spec . SessionAffinity )
}
// Verify that all the hosts belong to the same network, and retrieve their ID's.
lb . hostIDs , lb . networkID , err = cs . verifyHosts ( hosts )
if err != nil {
return nil , err
}
if ! lb . hasLoadBalancerIP ( ) {
// Create or retrieve the load balancer IP.
if err := lb . getLoadBalancerIP ( service . Spec . LoadBalancerIP ) ; err != nil {
return nil , err
}
if lb . ipAddr != "" && lb . ipAddr != service . Spec . LoadBalancerIP {
defer func ( lb * loadBalancer ) {
if err != nil {
if err := lb . releaseLoadBalancerIP ( ) ; err != nil {
glog . Errorf ( err . Error ( ) )
}
}
} ( lb )
}
}
glog . V ( 4 ) . Infof ( "Load balancer %v is associated with IP %v" , lb . name , lb . ipAddr )
for _ , port := range service . Spec . Ports {
// All ports have their own load balancer rule, so add the port to lbName to keep the names unique.
lbRuleName := fmt . Sprintf ( "%s-%d" , lb . name , port . Port )
// If the load balancer rule exists and is up-to-date, we move on to the next rule.
exists , needsUpdate , err := lb . checkLoadBalancerRule ( lbRuleName , port )
if err != nil {
return nil , err
}
if exists && ! needsUpdate {
glog . V ( 4 ) . Infof ( "Load balancer rule %v is up-to-date" , lbRuleName )
// Delete the rule from the map, to prevent it being deleted.
delete ( lb . rules , lbRuleName )
continue
}
if needsUpdate {
glog . V ( 4 ) . Infof ( "Updating load balancer rule: %v" , lbRuleName )
if err := lb . updateLoadBalancerRule ( lbRuleName ) ; err != nil {
return nil , err
}
// Delete the rule from the map, to prevent it being deleted.
delete ( lb . rules , lbRuleName )
continue
}
glog . V ( 4 ) . Infof ( "Creating load balancer rule: %v" , lbRuleName )
lbRule , err := lb . createLoadBalancerRule ( lbRuleName , port )
if err != nil {
return nil , err
}
glog . V ( 4 ) . Infof ( "Assigning hosts (%v) to load balancer rule: %v" , lb . hostIDs , lbRuleName )
if err = lb . assignHostsToRule ( lbRule , lb . hostIDs ) ; err != nil {
return nil , err
}
}
// Cleanup any rules that are now still in the rules map, as they are no longer needed.
for _ , lbRule := range lb . rules {
glog . V ( 4 ) . Infof ( "Deleting obsolete load balancer rule: %v" , lbRule . Name )
if err := lb . deleteLoadBalancerRule ( lbRule ) ; err != nil {
return nil , err
}
}
status = & api . LoadBalancerStatus { }
status . Ingress = [ ] api . LoadBalancerIngress { { IP : lb . ipAddr } }
return status , nil
}
// UpdateLoadBalancer updates hosts under the specified load balancer.
func ( cs * CSCloud ) UpdateLoadBalancer ( clusterName string , service * api . Service , hosts [ ] string ) error {
glog . V ( 4 ) . Infof ( "UpdateLoadBalancer(%v, %v, %v, %v)" , clusterName , service . Namespace , service . Name , hosts )
// Get the load balancer details and existing rules.
lb , err := cs . getLoadBalancer ( service )
if err != nil {
return err
}
// Verify that all the hosts belong to the same network, and retrieve their ID's.
lb . hostIDs , _ , err = cs . verifyHosts ( hosts )
if err != nil {
return err
}
for _ , lbRule := range lb . rules {
p := lb . LoadBalancer . NewListLoadBalancerRuleInstancesParams ( lbRule . Id )
// Retrieve all VMs currently associated to this load balancer rule.
l , err := lb . LoadBalancer . ListLoadBalancerRuleInstances ( p )
if err != nil {
return fmt . Errorf ( "error retrieving associated instances: %v" , err )
}
assign , remove := symmetricDifference ( lb . hostIDs , l . LoadBalancerRuleInstances )
if len ( assign ) > 0 {
glog . V ( 4 ) . Infof ( "Assigning new hosts (%v) to load balancer rule: %v" , assign , lbRule . Name )
if err := lb . assignHostsToRule ( lbRule , assign ) ; err != nil {
return err
}
}
if len ( remove ) > 0 {
glog . V ( 4 ) . Infof ( "Removing old hosts (%v) from load balancer rule: %v" , assign , lbRule . Name )
if err := lb . removeHostsFromRule ( lbRule , remove ) ; err != nil {
return err
}
}
}
return nil
}
// EnsureLoadBalancerDeleted deletes the specified load balancer if it exists, returning
// nil if the load balancer specified either didn't exist or was successfully deleted.
func ( cs * CSCloud ) EnsureLoadBalancerDeleted ( clusterName string , service * api . Service ) error {
glog . V ( 4 ) . Infof ( "EnsureLoadBalancerDeleted(%v, %v, %v)" , clusterName , service . Namespace , service . Name )
// Get the load balancer details and existing rules.
lb , err := cs . getLoadBalancer ( service )
if err != nil {
return err
}
for _ , lbRule := range lb . rules {
glog . V ( 4 ) . Infof ( "Deleting load balancer rule: %v" , lbRule . Name )
if err := lb . deleteLoadBalancerRule ( lbRule ) ; err != nil {
return err
}
}
2016-09-22 08:27:44 +00:00
if lb . ipAddr != "" && lb . ipAddr != service . Spec . LoadBalancerIP {
2016-07-28 14:20:26 +00:00
glog . V ( 4 ) . Infof ( "Releasing load balancer IP: %v" , lb . ipAddr )
if err := lb . releaseLoadBalancerIP ( ) ; err != nil {
return err
}
}
return nil
}
// getLoadBalancer retrieves the IP address and ID and all the existing rules it can find.
func ( cs * CSCloud ) getLoadBalancer ( service * api . Service ) ( * loadBalancer , error ) {
lb := & loadBalancer {
CloudStackClient : cs . client ,
name : cloudprovider . GetLoadBalancerName ( service ) ,
projectID : cs . projectID ,
rules : make ( map [ string ] * cloudstack . LoadBalancerRule ) ,
}
p := cs . client . LoadBalancer . NewListLoadBalancerRulesParams ( )
p . SetKeyword ( lb . name )
p . SetListall ( true )
if cs . projectID != "" {
p . SetProjectid ( cs . projectID )
}
l , err := cs . client . LoadBalancer . ListLoadBalancerRules ( p )
if err != nil {
return nil , fmt . Errorf ( "error retrieving load balancer rules: %v" , err )
}
for _ , lbRule := range l . LoadBalancerRules {
lb . rules [ lbRule . Name ] = lbRule
if lb . ipAddr != "" && lb . ipAddr != lbRule . Publicip {
glog . Warningf ( "Load balancer for service %v/%v has rules associated with different IP's: %v, %v" , service . Namespace , service . Name , lb . ipAddr , lbRule . Publicip )
}
lb . ipAddr = lbRule . Publicip
lb . ipAddrID = lbRule . Publicipid
}
glog . V ( 4 ) . Infof ( "Load balancer %v contains %d rule(s)" , lb . name , len ( lb . rules ) )
return lb , nil
}
2016-09-22 08:27:44 +00:00
// verifyHosts verifies if all hosts belong to the same network, and returns the host ID's and network ID.
2016-07-28 14:20:26 +00:00
func ( cs * CSCloud ) verifyHosts ( hosts [ ] string ) ( [ ] string , string , error ) {
2016-09-22 08:27:44 +00:00
hostNames := map [ string ] bool { }
2016-07-28 14:20:26 +00:00
for _ , host := range hosts {
2016-09-22 08:27:44 +00:00
hostNames [ host ] = true
2016-07-28 14:20:26 +00:00
}
p := cs . client . VirtualMachine . NewListVirtualMachinesParams ( )
p . SetListall ( true )
if cs . projectID != "" {
p . SetProjectid ( cs . projectID )
}
l , err := cs . client . VirtualMachine . ListVirtualMachines ( p )
if err != nil {
2016-09-22 08:27:44 +00:00
return nil , "" , fmt . Errorf ( "error retrieving list of hosts: %v" , err )
2016-07-28 14:20:26 +00:00
}
var hostIDs [ ] string
var networkID string
2016-09-22 08:27:44 +00:00
// Check if the virtual machine is in the hosts slice, then add the corresponding ID.
2016-07-28 14:20:26 +00:00
for _ , vm := range l . VirtualMachines {
2016-09-22 08:27:44 +00:00
if hostNames [ vm . Name ] {
2016-07-28 14:20:26 +00:00
if networkID != "" && networkID != vm . Nic [ 0 ] . Networkid {
return nil , "" , fmt . Errorf ( "found hosts that belong to different networks" )
}
networkID = vm . Nic [ 0 ] . Networkid
hostIDs = append ( hostIDs , vm . Id )
}
}
return hostIDs , networkID , nil
}
2016-09-22 08:27:44 +00:00
// hasLoadBalancerIP returns true if we have a load balancer address and ID.
2016-07-28 14:20:26 +00:00
func ( lb * loadBalancer ) hasLoadBalancerIP ( ) bool {
return lb . ipAddr != "" && lb . ipAddrID != ""
}
2016-09-22 08:27:44 +00:00
// getLoadBalancerIP retieves an existing IP or associates a new IP.
2016-07-28 14:20:26 +00:00
func ( lb * loadBalancer ) getLoadBalancerIP ( loadBalancerIP string ) error {
if loadBalancerIP != "" {
return lb . getPublicIPAddress ( loadBalancerIP )
}
return lb . associatePublicIPAddress ( )
}
2016-09-22 08:27:44 +00:00
// getPublicIPAddressID retrieves the ID of the given IP, and sets the address and it's ID.
2016-07-28 14:20:26 +00:00
func ( lb * loadBalancer ) getPublicIPAddress ( loadBalancerIP string ) error {
glog . V ( 4 ) . Infof ( "Retrieve load balancer IP details: %v" , loadBalancerIP )
p := lb . Address . NewListPublicIpAddressesParams ( )
p . SetIpaddress ( loadBalancerIP )
p . SetListall ( true )
if lb . projectID != "" {
p . SetProjectid ( lb . projectID )
}
l , err := lb . Address . ListPublicIpAddresses ( p )
if err != nil {
2016-09-22 08:27:44 +00:00
return fmt . Errorf ( "error retrieving IP address: %v" , err )
2016-07-28 14:20:26 +00:00
}
if l . Count != 1 {
return fmt . Errorf ( "could not find IP address %v" , loadBalancerIP )
}
lb . ipAddr = l . PublicIpAddresses [ 0 ] . Ipaddress
lb . ipAddrID = l . PublicIpAddresses [ 0 ] . Id
return nil
}
2016-09-22 08:27:44 +00:00
// associatePublicIPAddress associates a new IP and sets the address and it's ID.
2016-07-28 14:20:26 +00:00
func ( lb * loadBalancer ) associatePublicIPAddress ( ) error {
glog . V ( 4 ) . Infof ( "Allocate new IP for load balancer: %v" , lb . name )
// If a network belongs to a VPC, the IP address needs to be associated with
// the VPC instead of with the network.
network , count , err := lb . Network . GetNetworkByID ( lb . networkID , cloudstack . WithProject ( lb . projectID ) )
if err != nil {
if count == 0 {
return fmt . Errorf ( "could not find network %v" , lb . networkID )
}
return fmt . Errorf ( "error retrieving network: %v" , err )
}
p := lb . Address . NewAssociateIpAddressParams ( )
if network . Vpcid != "" {
p . SetVpcid ( network . Vpcid )
} else {
p . SetNetworkid ( lb . networkID )
}
if lb . projectID != "" {
p . SetProjectid ( lb . projectID )
}
// Associate a new IP address
r , err := lb . Address . AssociateIpAddress ( p )
if err != nil {
2016-09-22 08:27:44 +00:00
return fmt . Errorf ( "error associating new IP address: %v" , err )
2016-07-28 14:20:26 +00:00
}
lb . ipAddr = r . Ipaddress
lb . ipAddrID = r . Id
return nil
}
// releasePublicIPAddress releases an associated IP.
func ( lb * loadBalancer ) releaseLoadBalancerIP ( ) error {
p := lb . Address . NewDisassociateIpAddressParams ( lb . ipAddrID )
if _ , err := lb . Address . DisassociateIpAddress ( p ) ; err != nil {
return fmt . Errorf ( "error releasing load balancer IP %v: %v" , lb . ipAddr , err )
}
return nil
}
// checkLoadBalancerRule checks if the rule already exists and if it does, if it can be updated. If
// it does exist but cannot be updated, it will delete the existing rule so it can be created again.
func ( lb * loadBalancer ) checkLoadBalancerRule ( lbRuleName string , port api . ServicePort ) ( bool , bool , error ) {
lbRule , ok := lb . rules [ lbRuleName ]
if ! ok {
return false , false , nil
}
// Check if any of the values we cannot update (those that require a new load balancer rule) are changed.
if lbRule . Publicip == lb . ipAddr && lbRule . Privateport == strconv . Itoa ( int ( port . NodePort ) ) && lbRule . Publicport == strconv . Itoa ( int ( port . Port ) ) {
return true , lbRule . Algorithm != lb . algorithm , nil
}
// Delete the load balancer rule so we can create a new one using the new values.
if err := lb . deleteLoadBalancerRule ( lbRule ) ; err != nil {
return false , false , err
}
return false , false , nil
}
// updateLoadBalancerRule updates a load balancer rule.
func ( lb * loadBalancer ) updateLoadBalancerRule ( lbRuleName string ) error {
lbRule := lb . rules [ lbRuleName ]
p := lb . LoadBalancer . NewUpdateLoadBalancerRuleParams ( lbRule . Id )
p . SetAlgorithm ( lb . algorithm )
_ , err := lb . LoadBalancer . UpdateLoadBalancerRule ( p )
return err
}
// createLoadBalancerRule creates a new load balancer rule and returns it's ID.
func ( lb * loadBalancer ) createLoadBalancerRule ( lbRuleName string , port api . ServicePort ) ( * cloudstack . LoadBalancerRule , error ) {
p := lb . LoadBalancer . NewCreateLoadBalancerRuleParams (
lb . algorithm ,
lbRuleName ,
int ( port . NodePort ) ,
int ( port . Port ) ,
)
p . SetNetworkid ( lb . networkID )
p . SetPublicipid ( lb . ipAddrID )
switch port . Protocol {
case api . ProtocolTCP :
p . SetProtocol ( "TCP" )
case api . ProtocolUDP :
p . SetProtocol ( "UDP" )
default :
return nil , fmt . Errorf ( "unsupported load balancer protocol: %v" , port . Protocol )
}
// Do not create corresponding firewall rule.
p . SetOpenfirewall ( false )
// Create a new load balancer rule.
r , err := lb . LoadBalancer . CreateLoadBalancerRule ( p )
if err != nil {
2016-09-22 08:27:44 +00:00
return nil , fmt . Errorf ( "error creating load balancer rule %v: %v" , lbRuleName , err )
2016-07-28 14:20:26 +00:00
}
lbRule := & cloudstack . LoadBalancerRule {
Id : r . Id ,
Algorithm : r . Algorithm ,
Cidrlist : r . Cidrlist ,
Name : r . Name ,
Networkid : r . Networkid ,
Privateport : r . Privateport ,
Publicport : r . Publicport ,
Publicip : r . Publicip ,
Publicipid : r . Publicipid ,
}
return lbRule , nil
}
// deleteLoadBalancerRule deletes a load balancer rule.
func ( lb * loadBalancer ) deleteLoadBalancerRule ( lbRule * cloudstack . LoadBalancerRule ) error {
p := lb . LoadBalancer . NewDeleteLoadBalancerRuleParams ( lbRule . Id )
if _ , err := lb . LoadBalancer . DeleteLoadBalancerRule ( p ) ; err != nil {
return fmt . Errorf ( "error deleting load balancer rule %v: %v" , lbRule . Name , err )
}
// Delete the rule from the map as it no longer exists
delete ( lb . rules , lbRule . Name )
return nil
}
// assignHostsToRule assigns hosts to a load balancer rule.
func ( lb * loadBalancer ) assignHostsToRule ( lbRule * cloudstack . LoadBalancerRule , hostIDs [ ] string ) error {
p := lb . LoadBalancer . NewAssignToLoadBalancerRuleParams ( lbRule . Id )
p . SetVirtualmachineids ( hostIDs )
if _ , err := lb . LoadBalancer . AssignToLoadBalancerRule ( p ) ; err != nil {
return fmt . Errorf ( "error assigning hosts to load balancer rule %v: %v" , lbRule . Name , err )
}
return nil
}
// removeHostsFromRule removes hosts from a load balancer rule.
func ( lb * loadBalancer ) removeHostsFromRule ( lbRule * cloudstack . LoadBalancerRule , hostIDs [ ] string ) error {
p := lb . LoadBalancer . NewRemoveFromLoadBalancerRuleParams ( lbRule . Id )
p . SetVirtualmachineids ( hostIDs )
if _ , err := lb . LoadBalancer . RemoveFromLoadBalancerRule ( p ) ; err != nil {
return fmt . Errorf ( "error removing hosts from load balancer rule %v: %v" , lbRule . Name , err )
}
return nil
}
// symmetricDifference returns the symmetric difference between the old (existing) and new (wanted) host ID's.
func symmetricDifference ( hostIDs [ ] string , lbInstances [ ] * cloudstack . VirtualMachine ) ( [ ] string , [ ] string ) {
new := make ( map [ string ] bool )
for _ , hostID := range hostIDs {
new [ hostID ] = true
}
var remove [ ] string
for _ , instance := range lbInstances {
if new [ instance . Id ] {
delete ( new , instance . Id )
continue
}
remove = append ( remove , instance . Id )
}
var assign [ ] string
for hostID := range new {
assign = append ( assign , hostID )
}
return assign , remove
}