mirror of
https://github.com/k3s-io/k3s.git
synced 2024-06-07 19:41:36 +00:00
938 lines
27 KiB
Go
938 lines
27 KiB
Go
|
// +build !providerless
|
||
|
|
||
|
/*
|
||
|
Copyright 2014 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 openstack
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"crypto/tls"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"net"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"reflect"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/gophercloud/gophercloud"
|
||
|
"github.com/gophercloud/gophercloud/openstack"
|
||
|
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces"
|
||
|
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
|
||
|
"github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts"
|
||
|
tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
|
||
|
"github.com/gophercloud/gophercloud/pagination"
|
||
|
"github.com/mitchellh/mapstructure"
|
||
|
"gopkg.in/gcfg.v1"
|
||
|
|
||
|
v1 "k8s.io/api/core/v1"
|
||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||
|
"k8s.io/apimachinery/pkg/types"
|
||
|
netutil "k8s.io/apimachinery/pkg/util/net"
|
||
|
"k8s.io/client-go/kubernetes"
|
||
|
"k8s.io/client-go/tools/clientcmd"
|
||
|
certutil "k8s.io/client-go/util/cert"
|
||
|
cloudprovider "k8s.io/cloud-provider"
|
||
|
nodehelpers "k8s.io/cloud-provider/node/helpers"
|
||
|
"k8s.io/klog"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// ProviderName is the name of the openstack provider
|
||
|
ProviderName = "openstack"
|
||
|
|
||
|
// TypeHostName is the name type of openstack instance
|
||
|
TypeHostName = "hostname"
|
||
|
availabilityZone = "availability_zone"
|
||
|
defaultTimeOut = 60 * time.Second
|
||
|
)
|
||
|
|
||
|
// ErrNotFound is used to inform that the object is missing
|
||
|
var ErrNotFound = errors.New("failed to find object")
|
||
|
|
||
|
// ErrMultipleResults is used when we unexpectedly get back multiple results
|
||
|
var ErrMultipleResults = errors.New("multiple results where only one expected")
|
||
|
|
||
|
// ErrNoAddressFound is used when we cannot find an ip address for the host
|
||
|
var ErrNoAddressFound = errors.New("no address found for host")
|
||
|
|
||
|
// MyDuration is the encoding.TextUnmarshaler interface for time.Duration
|
||
|
type MyDuration struct {
|
||
|
time.Duration
|
||
|
}
|
||
|
|
||
|
// UnmarshalText is used to convert from text to Duration
|
||
|
func (d *MyDuration) UnmarshalText(text []byte) error {
|
||
|
res, err := time.ParseDuration(string(text))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
d.Duration = res
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// LoadBalancer is used for creating and maintaining load balancers
|
||
|
type LoadBalancer struct {
|
||
|
network *gophercloud.ServiceClient
|
||
|
compute *gophercloud.ServiceClient
|
||
|
lb *gophercloud.ServiceClient
|
||
|
opts LoadBalancerOpts
|
||
|
}
|
||
|
|
||
|
// LoadBalancerOpts have the options to talk to Neutron LBaaSV2 or Octavia
|
||
|
type LoadBalancerOpts struct {
|
||
|
LBVersion string `gcfg:"lb-version"` // overrides autodetection. Only support v2.
|
||
|
UseOctavia bool `gcfg:"use-octavia"` // uses Octavia V2 service catalog endpoint
|
||
|
SubnetID string `gcfg:"subnet-id"` // overrides autodetection.
|
||
|
FloatingNetworkID string `gcfg:"floating-network-id"` // If specified, will create floating ip for loadbalancer, or do not create floating ip.
|
||
|
LBMethod string `gcfg:"lb-method"` // default to ROUND_ROBIN.
|
||
|
LBProvider string `gcfg:"lb-provider"`
|
||
|
CreateMonitor bool `gcfg:"create-monitor"`
|
||
|
MonitorDelay MyDuration `gcfg:"monitor-delay"`
|
||
|
MonitorTimeout MyDuration `gcfg:"monitor-timeout"`
|
||
|
MonitorMaxRetries uint `gcfg:"monitor-max-retries"`
|
||
|
ManageSecurityGroups bool `gcfg:"manage-security-groups"`
|
||
|
NodeSecurityGroupIDs []string // Do not specify, get it automatically when enable manage-security-groups. TODO(FengyunPan): move it into cache
|
||
|
}
|
||
|
|
||
|
// BlockStorageOpts is used to talk to Cinder service
|
||
|
type BlockStorageOpts struct {
|
||
|
BSVersion string `gcfg:"bs-version"` // overrides autodetection. v1 or v2. Defaults to auto
|
||
|
TrustDevicePath bool `gcfg:"trust-device-path"` // See Issue #33128
|
||
|
IgnoreVolumeAZ bool `gcfg:"ignore-volume-az"`
|
||
|
NodeVolumeAttachLimit int `gcfg:"node-volume-attach-limit"` // override volume attach limit for Cinder. Default is : 256
|
||
|
}
|
||
|
|
||
|
// RouterOpts is used for Neutron routes
|
||
|
type RouterOpts struct {
|
||
|
RouterID string `gcfg:"router-id"` // required
|
||
|
}
|
||
|
|
||
|
// MetadataOpts is used for configuring how to talk to metadata service or config drive
|
||
|
type MetadataOpts struct {
|
||
|
SearchOrder string `gcfg:"search-order"`
|
||
|
RequestTimeout MyDuration `gcfg:"request-timeout"`
|
||
|
}
|
||
|
|
||
|
var _ cloudprovider.Interface = (*OpenStack)(nil)
|
||
|
var _ cloudprovider.Zones = (*OpenStack)(nil)
|
||
|
|
||
|
// OpenStack is an implementation of cloud provider Interface for OpenStack.
|
||
|
type OpenStack struct {
|
||
|
provider *gophercloud.ProviderClient
|
||
|
region string
|
||
|
lbOpts LoadBalancerOpts
|
||
|
bsOpts BlockStorageOpts
|
||
|
routeOpts RouterOpts
|
||
|
metadataOpts MetadataOpts
|
||
|
// InstanceID of the server where this OpenStack object is instantiated.
|
||
|
localInstanceID string
|
||
|
}
|
||
|
|
||
|
// Config is used to read and store information from the cloud configuration file
|
||
|
// NOTE: Cloud config files should follow the same Kubernetes deprecation policy as
|
||
|
// flags or CLIs. Config fields should not change behavior in incompatible ways and
|
||
|
// should be deprecated for at least 2 release prior to removing.
|
||
|
// See https://kubernetes.io/docs/reference/using-api/deprecation-policy/#deprecating-a-flag-or-cli
|
||
|
// for more details.
|
||
|
type Config struct {
|
||
|
Global struct {
|
||
|
AuthURL string `gcfg:"auth-url"`
|
||
|
Username string
|
||
|
UserID string `gcfg:"user-id"`
|
||
|
Password string
|
||
|
TenantID string `gcfg:"tenant-id"`
|
||
|
TenantName string `gcfg:"tenant-name"`
|
||
|
TrustID string `gcfg:"trust-id"`
|
||
|
DomainID string `gcfg:"domain-id"`
|
||
|
DomainName string `gcfg:"domain-name"`
|
||
|
Region string
|
||
|
CAFile string `gcfg:"ca-file"`
|
||
|
SecretName string `gcfg:"secret-name"`
|
||
|
SecretNamespace string `gcfg:"secret-namespace"`
|
||
|
KubeconfigPath string `gcfg:"kubeconfig-path"`
|
||
|
}
|
||
|
LoadBalancer LoadBalancerOpts
|
||
|
BlockStorage BlockStorageOpts
|
||
|
Route RouterOpts
|
||
|
Metadata MetadataOpts
|
||
|
}
|
||
|
|
||
|
func init() {
|
||
|
registerMetrics()
|
||
|
|
||
|
cloudprovider.RegisterCloudProvider(ProviderName, func(config io.Reader) (cloudprovider.Interface, error) {
|
||
|
cfg, err := readConfig(config)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return newOpenStack(cfg)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (cfg Config) toAuthOptions() gophercloud.AuthOptions {
|
||
|
return gophercloud.AuthOptions{
|
||
|
IdentityEndpoint: cfg.Global.AuthURL,
|
||
|
Username: cfg.Global.Username,
|
||
|
UserID: cfg.Global.UserID,
|
||
|
Password: cfg.Global.Password,
|
||
|
TenantID: cfg.Global.TenantID,
|
||
|
TenantName: cfg.Global.TenantName,
|
||
|
DomainID: cfg.Global.DomainID,
|
||
|
DomainName: cfg.Global.DomainName,
|
||
|
|
||
|
// Persistent service, so we need to be able to renew tokens.
|
||
|
AllowReauth: true,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (cfg Config) toAuth3Options() tokens3.AuthOptions {
|
||
|
return tokens3.AuthOptions{
|
||
|
IdentityEndpoint: cfg.Global.AuthURL,
|
||
|
Username: cfg.Global.Username,
|
||
|
UserID: cfg.Global.UserID,
|
||
|
Password: cfg.Global.Password,
|
||
|
DomainID: cfg.Global.DomainID,
|
||
|
DomainName: cfg.Global.DomainName,
|
||
|
AllowReauth: true,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// configFromEnv allows setting up credentials etc using the
|
||
|
// standard OS_* OpenStack client environment variables.
|
||
|
func configFromEnv() (cfg Config, ok bool) {
|
||
|
cfg.Global.AuthURL = os.Getenv("OS_AUTH_URL")
|
||
|
cfg.Global.Username = os.Getenv("OS_USERNAME")
|
||
|
cfg.Global.Password = os.Getenv("OS_PASSWORD")
|
||
|
cfg.Global.Region = os.Getenv("OS_REGION_NAME")
|
||
|
cfg.Global.UserID = os.Getenv("OS_USER_ID")
|
||
|
cfg.Global.TrustID = os.Getenv("OS_TRUST_ID")
|
||
|
|
||
|
cfg.Global.TenantID = os.Getenv("OS_TENANT_ID")
|
||
|
if cfg.Global.TenantID == "" {
|
||
|
cfg.Global.TenantID = os.Getenv("OS_PROJECT_ID")
|
||
|
}
|
||
|
cfg.Global.TenantName = os.Getenv("OS_TENANT_NAME")
|
||
|
if cfg.Global.TenantName == "" {
|
||
|
cfg.Global.TenantName = os.Getenv("OS_PROJECT_NAME")
|
||
|
}
|
||
|
|
||
|
cfg.Global.DomainID = os.Getenv("OS_DOMAIN_ID")
|
||
|
if cfg.Global.DomainID == "" {
|
||
|
cfg.Global.DomainID = os.Getenv("OS_USER_DOMAIN_ID")
|
||
|
}
|
||
|
cfg.Global.DomainName = os.Getenv("OS_DOMAIN_NAME")
|
||
|
if cfg.Global.DomainName == "" {
|
||
|
cfg.Global.DomainName = os.Getenv("OS_USER_DOMAIN_NAME")
|
||
|
}
|
||
|
|
||
|
cfg.Global.SecretName = os.Getenv("SECRET_NAME")
|
||
|
cfg.Global.SecretNamespace = os.Getenv("SECRET_NAMESPACE")
|
||
|
cfg.Global.KubeconfigPath = os.Getenv("KUBECONFIG_PATH")
|
||
|
|
||
|
ok = cfg.Global.AuthURL != "" &&
|
||
|
cfg.Global.Username != "" &&
|
||
|
cfg.Global.Password != "" &&
|
||
|
(cfg.Global.TenantID != "" || cfg.Global.TenantName != "" ||
|
||
|
cfg.Global.DomainID != "" || cfg.Global.DomainName != "" ||
|
||
|
cfg.Global.Region != "" || cfg.Global.UserID != "" ||
|
||
|
cfg.Global.TrustID != "")
|
||
|
|
||
|
cfg.Metadata.SearchOrder = fmt.Sprintf("%s,%s", configDriveID, metadataID)
|
||
|
cfg.BlockStorage.BSVersion = "auto"
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func createKubernetesClient(kubeconfigPath string) (*kubernetes.Clientset, error) {
|
||
|
klog.Info("Creating kubernetes API client.")
|
||
|
|
||
|
cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
cfg.DisableCompression = true
|
||
|
|
||
|
client, err := kubernetes.NewForConfig(cfg)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
v, err := client.Discovery().ServerVersion()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
klog.Infof("Kubernetes API client created, server version %s", fmt.Sprintf("v%v.%v", v.Major, v.Minor))
|
||
|
return client, nil
|
||
|
}
|
||
|
|
||
|
// setConfigFromSecret allows setting up the config from k8s secret
|
||
|
func setConfigFromSecret(cfg *Config) error {
|
||
|
secretName := cfg.Global.SecretName
|
||
|
secretNamespace := cfg.Global.SecretNamespace
|
||
|
kubeconfigPath := cfg.Global.KubeconfigPath
|
||
|
|
||
|
k8sClient, err := createKubernetesClient(kubeconfigPath)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("failed to get kubernetes client: %v", err)
|
||
|
}
|
||
|
|
||
|
secret, err := k8sClient.CoreV1().Secrets(secretNamespace).Get(secretName, metav1.GetOptions{})
|
||
|
if err != nil {
|
||
|
klog.Warningf("Cannot get secret %s in namespace %s. error: %q", secretName, secretNamespace, err)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if content, ok := secret.Data["clouds.conf"]; ok {
|
||
|
err = gcfg.ReadStringInto(cfg, string(content))
|
||
|
if err != nil {
|
||
|
klog.Error("Cannot parse data from the secret.")
|
||
|
return fmt.Errorf("cannot parse data from the secret")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
klog.Error("Cannot find \"clouds.conf\" key in the secret.")
|
||
|
return fmt.Errorf("cannot find \"clouds.conf\" key in the secret")
|
||
|
}
|
||
|
|
||
|
func readConfig(config io.Reader) (Config, error) {
|
||
|
if config == nil {
|
||
|
return Config{}, fmt.Errorf("no OpenStack cloud provider config file given")
|
||
|
}
|
||
|
|
||
|
cfg, _ := configFromEnv()
|
||
|
|
||
|
// Set default values for config params
|
||
|
cfg.BlockStorage.BSVersion = "auto"
|
||
|
cfg.BlockStorage.TrustDevicePath = false
|
||
|
cfg.BlockStorage.IgnoreVolumeAZ = false
|
||
|
cfg.Metadata.SearchOrder = fmt.Sprintf("%s,%s", configDriveID, metadataID)
|
||
|
|
||
|
err := gcfg.ReadInto(&cfg, config)
|
||
|
if err != nil {
|
||
|
return cfg, err
|
||
|
}
|
||
|
|
||
|
if cfg.Global.SecretName != "" && cfg.Global.SecretNamespace != "" {
|
||
|
klog.Infof("Set credentials from secret %s in namespace %s", cfg.Global.SecretName, cfg.Global.SecretNamespace)
|
||
|
err = setConfigFromSecret(&cfg)
|
||
|
if err != nil {
|
||
|
return cfg, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return cfg, nil
|
||
|
}
|
||
|
|
||
|
// caller is a tiny helper for conditional unwind logic
|
||
|
type caller bool
|
||
|
|
||
|
func newCaller() caller { return caller(true) }
|
||
|
func (c *caller) disarm() { *c = false }
|
||
|
|
||
|
func (c *caller) call(f func()) {
|
||
|
if *c {
|
||
|
f()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func readInstanceID(searchOrder string) (string, error) {
|
||
|
// Try to find instance ID on the local filesystem (created by cloud-init)
|
||
|
const instanceIDFile = "/var/lib/cloud/data/instance-id"
|
||
|
idBytes, err := ioutil.ReadFile(instanceIDFile)
|
||
|
if err == nil {
|
||
|
instanceID := string(idBytes)
|
||
|
instanceID = strings.TrimSpace(instanceID)
|
||
|
klog.V(3).Infof("Got instance id from %s: %s", instanceIDFile, instanceID)
|
||
|
if instanceID != "" {
|
||
|
return instanceID, nil
|
||
|
}
|
||
|
// Fall through to metadata server lookup
|
||
|
}
|
||
|
|
||
|
md, err := getMetadata(searchOrder)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
return md.UUID, nil
|
||
|
}
|
||
|
|
||
|
// check opts for OpenStack
|
||
|
func checkOpenStackOpts(openstackOpts *OpenStack) error {
|
||
|
lbOpts := openstackOpts.lbOpts
|
||
|
|
||
|
// if need to create health monitor for Neutron LB,
|
||
|
// monitor-delay, monitor-timeout and monitor-max-retries should be set.
|
||
|
emptyDuration := MyDuration{}
|
||
|
if lbOpts.CreateMonitor {
|
||
|
if lbOpts.MonitorDelay == emptyDuration {
|
||
|
return fmt.Errorf("monitor-delay not set in cloud provider config")
|
||
|
}
|
||
|
if lbOpts.MonitorTimeout == emptyDuration {
|
||
|
return fmt.Errorf("monitor-timeout not set in cloud provider config")
|
||
|
}
|
||
|
if lbOpts.MonitorMaxRetries == uint(0) {
|
||
|
return fmt.Errorf("monitor-max-retries not set in cloud provider config")
|
||
|
}
|
||
|
}
|
||
|
return checkMetadataSearchOrder(openstackOpts.metadataOpts.SearchOrder)
|
||
|
}
|
||
|
|
||
|
func newOpenStack(cfg Config) (*OpenStack, error) {
|
||
|
provider, err := openstack.NewClient(cfg.Global.AuthURL)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if cfg.Global.CAFile != "" {
|
||
|
roots, err := certutil.NewPool(cfg.Global.CAFile)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
config := &tls.Config{}
|
||
|
config.RootCAs = roots
|
||
|
provider.HTTPClient.Transport = netutil.SetOldTransportDefaults(&http.Transport{TLSClientConfig: config})
|
||
|
|
||
|
}
|
||
|
if cfg.Global.TrustID != "" {
|
||
|
opts := cfg.toAuth3Options()
|
||
|
authOptsExt := trusts.AuthOptsExt{
|
||
|
TrustID: cfg.Global.TrustID,
|
||
|
AuthOptionsBuilder: &opts,
|
||
|
}
|
||
|
err = openstack.AuthenticateV3(provider, authOptsExt, gophercloud.EndpointOpts{})
|
||
|
} else {
|
||
|
err = openstack.Authenticate(provider, cfg.toAuthOptions())
|
||
|
}
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
emptyDuration := MyDuration{}
|
||
|
if cfg.Metadata.RequestTimeout == emptyDuration {
|
||
|
cfg.Metadata.RequestTimeout.Duration = time.Duration(defaultTimeOut)
|
||
|
}
|
||
|
provider.HTTPClient.Timeout = cfg.Metadata.RequestTimeout.Duration
|
||
|
|
||
|
os := OpenStack{
|
||
|
provider: provider,
|
||
|
region: cfg.Global.Region,
|
||
|
lbOpts: cfg.LoadBalancer,
|
||
|
bsOpts: cfg.BlockStorage,
|
||
|
routeOpts: cfg.Route,
|
||
|
metadataOpts: cfg.Metadata,
|
||
|
}
|
||
|
|
||
|
err = checkOpenStackOpts(&os)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &os, nil
|
||
|
}
|
||
|
|
||
|
// NewFakeOpenStackCloud creates and returns an instance of Openstack cloudprovider.
|
||
|
// Mainly for use in tests that require instantiating Openstack without having
|
||
|
// to go through cloudprovider interface.
|
||
|
func NewFakeOpenStackCloud(cfg Config) (*OpenStack, error) {
|
||
|
provider, err := openstack.NewClient(cfg.Global.AuthURL)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
emptyDuration := MyDuration{}
|
||
|
if cfg.Metadata.RequestTimeout == emptyDuration {
|
||
|
cfg.Metadata.RequestTimeout.Duration = time.Duration(defaultTimeOut)
|
||
|
}
|
||
|
provider.HTTPClient.Timeout = cfg.Metadata.RequestTimeout.Duration
|
||
|
|
||
|
os := OpenStack{
|
||
|
provider: provider,
|
||
|
region: cfg.Global.Region,
|
||
|
lbOpts: cfg.LoadBalancer,
|
||
|
bsOpts: cfg.BlockStorage,
|
||
|
routeOpts: cfg.Route,
|
||
|
metadataOpts: cfg.Metadata,
|
||
|
}
|
||
|
|
||
|
return &os, nil
|
||
|
}
|
||
|
|
||
|
// Initialize passes a Kubernetes clientBuilder interface to the cloud provider
|
||
|
func (os *OpenStack) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stop <-chan struct{}) {
|
||
|
}
|
||
|
|
||
|
// mapNodeNameToServerName maps a k8s NodeName to an OpenStack Server Name
|
||
|
// This is a simple string cast.
|
||
|
func mapNodeNameToServerName(nodeName types.NodeName) string {
|
||
|
return string(nodeName)
|
||
|
}
|
||
|
|
||
|
// GetNodeNameByID maps instanceid to types.NodeName
|
||
|
func (os *OpenStack) GetNodeNameByID(instanceID string) (types.NodeName, error) {
|
||
|
client, err := os.NewComputeV2()
|
||
|
var nodeName types.NodeName
|
||
|
if err != nil {
|
||
|
return nodeName, err
|
||
|
}
|
||
|
|
||
|
server, err := servers.Get(client, instanceID).Extract()
|
||
|
if err != nil {
|
||
|
return nodeName, err
|
||
|
}
|
||
|
nodeName = mapServerToNodeName(server)
|
||
|
return nodeName, nil
|
||
|
}
|
||
|
|
||
|
// mapServerToNodeName maps an OpenStack Server to a k8s NodeName
|
||
|
func mapServerToNodeName(server *servers.Server) types.NodeName {
|
||
|
// Node names are always lowercase, and (at least)
|
||
|
// routecontroller does case-sensitive string comparisons
|
||
|
// assuming this
|
||
|
return types.NodeName(strings.ToLower(server.Name))
|
||
|
}
|
||
|
|
||
|
func foreachServer(client *gophercloud.ServiceClient, opts servers.ListOptsBuilder, handler func(*servers.Server) (bool, error)) error {
|
||
|
pager := servers.List(client, opts)
|
||
|
|
||
|
err := pager.EachPage(func(page pagination.Page) (bool, error) {
|
||
|
s, err := servers.ExtractServers(page)
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
for _, server := range s {
|
||
|
ok, err := handler(&server)
|
||
|
if !ok || err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
}
|
||
|
return true, nil
|
||
|
})
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func getServerByName(client *gophercloud.ServiceClient, name types.NodeName) (*servers.Server, error) {
|
||
|
opts := servers.ListOpts{
|
||
|
Name: fmt.Sprintf("^%s$", regexp.QuoteMeta(mapNodeNameToServerName(name))),
|
||
|
}
|
||
|
|
||
|
pager := servers.List(client, opts)
|
||
|
|
||
|
serverList := make([]servers.Server, 0, 1)
|
||
|
|
||
|
err := pager.EachPage(func(page pagination.Page) (bool, error) {
|
||
|
s, err := servers.ExtractServers(page)
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
serverList = append(serverList, s...)
|
||
|
if len(serverList) > 1 {
|
||
|
return false, ErrMultipleResults
|
||
|
}
|
||
|
return true, nil
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if len(serverList) == 0 {
|
||
|
return nil, ErrNotFound
|
||
|
}
|
||
|
|
||
|
return &serverList[0], nil
|
||
|
}
|
||
|
|
||
|
func nodeAddresses(srv *servers.Server) ([]v1.NodeAddress, error) {
|
||
|
addrs := []v1.NodeAddress{}
|
||
|
|
||
|
type Address struct {
|
||
|
IPType string `mapstructure:"OS-EXT-IPS:type"`
|
||
|
Addr string
|
||
|
}
|
||
|
|
||
|
var addresses map[string][]Address
|
||
|
err := mapstructure.Decode(srv.Addresses, &addresses)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
for network, addrList := range addresses {
|
||
|
for _, props := range addrList {
|
||
|
var addressType v1.NodeAddressType
|
||
|
if props.IPType == "floating" || network == "public" {
|
||
|
addressType = v1.NodeExternalIP
|
||
|
} else {
|
||
|
addressType = v1.NodeInternalIP
|
||
|
}
|
||
|
|
||
|
nodehelpers.AddToNodeAddresses(&addrs,
|
||
|
v1.NodeAddress{
|
||
|
Type: addressType,
|
||
|
Address: props.Addr,
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// AccessIPs are usually duplicates of "public" addresses.
|
||
|
if srv.AccessIPv4 != "" {
|
||
|
nodehelpers.AddToNodeAddresses(&addrs,
|
||
|
v1.NodeAddress{
|
||
|
Type: v1.NodeExternalIP,
|
||
|
Address: srv.AccessIPv4,
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if srv.AccessIPv6 != "" {
|
||
|
nodehelpers.AddToNodeAddresses(&addrs,
|
||
|
v1.NodeAddress{
|
||
|
Type: v1.NodeExternalIP,
|
||
|
Address: srv.AccessIPv6,
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if srv.Metadata[TypeHostName] != "" {
|
||
|
nodehelpers.AddToNodeAddresses(&addrs,
|
||
|
v1.NodeAddress{
|
||
|
Type: v1.NodeHostName,
|
||
|
Address: srv.Metadata[TypeHostName],
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return addrs, nil
|
||
|
}
|
||
|
|
||
|
func getAddressesByName(client *gophercloud.ServiceClient, name types.NodeName) ([]v1.NodeAddress, error) {
|
||
|
srv, err := getServerByName(client, name)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return nodeAddresses(srv)
|
||
|
}
|
||
|
|
||
|
func getAddressByName(client *gophercloud.ServiceClient, name types.NodeName, needIPv6 bool) (string, error) {
|
||
|
addrs, err := getAddressesByName(client, name)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
} else if len(addrs) == 0 {
|
||
|
return "", ErrNoAddressFound
|
||
|
}
|
||
|
|
||
|
for _, addr := range addrs {
|
||
|
isIPv6 := net.ParseIP(addr.Address).To4() == nil
|
||
|
if (addr.Type == v1.NodeInternalIP) && (isIPv6 == needIPv6) {
|
||
|
return addr.Address, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for _, addr := range addrs {
|
||
|
isIPv6 := net.ParseIP(addr.Address).To4() == nil
|
||
|
if (addr.Type == v1.NodeExternalIP) && (isIPv6 == needIPv6) {
|
||
|
return addr.Address, nil
|
||
|
}
|
||
|
}
|
||
|
// It should never return an address from a different IP Address family than the one needed
|
||
|
return "", ErrNoAddressFound
|
||
|
}
|
||
|
|
||
|
// getAttachedInterfacesByID returns the node interfaces of the specified instance.
|
||
|
func getAttachedInterfacesByID(client *gophercloud.ServiceClient, serviceID string) ([]attachinterfaces.Interface, error) {
|
||
|
var interfaces []attachinterfaces.Interface
|
||
|
|
||
|
pager := attachinterfaces.List(client, serviceID)
|
||
|
err := pager.EachPage(func(page pagination.Page) (bool, error) {
|
||
|
s, err := attachinterfaces.ExtractInterfaces(page)
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
interfaces = append(interfaces, s...)
|
||
|
return true, nil
|
||
|
})
|
||
|
if err != nil {
|
||
|
return interfaces, err
|
||
|
}
|
||
|
|
||
|
return interfaces, nil
|
||
|
}
|
||
|
|
||
|
// Clusters is a no-op
|
||
|
func (os *OpenStack) Clusters() (cloudprovider.Clusters, bool) {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
// ProviderName returns the cloud provider ID.
|
||
|
func (os *OpenStack) ProviderName() string {
|
||
|
return ProviderName
|
||
|
}
|
||
|
|
||
|
// HasClusterID returns true if the cluster has a clusterID
|
||
|
func (os *OpenStack) HasClusterID() bool {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// LoadBalancer initializes a LbaasV2 object
|
||
|
func (os *OpenStack) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
|
||
|
klog.V(4).Info("openstack.LoadBalancer() called")
|
||
|
|
||
|
if reflect.DeepEqual(os.lbOpts, LoadBalancerOpts{}) {
|
||
|
klog.V(4).Info("LoadBalancer section is empty/not defined in cloud-config")
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
network, err := os.NewNetworkV2()
|
||
|
if err != nil {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
compute, err := os.NewComputeV2()
|
||
|
if err != nil {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
lb, err := os.NewLoadBalancerV2()
|
||
|
if err != nil {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
// LBaaS v1 is deprecated in the OpenStack Liberty release.
|
||
|
// Currently kubernetes OpenStack cloud provider just support LBaaS v2.
|
||
|
lbVersion := os.lbOpts.LBVersion
|
||
|
if lbVersion != "" && lbVersion != "v2" {
|
||
|
klog.Warningf("Config error: currently only support LBaaS v2, unrecognised lb-version \"%v\"", lbVersion)
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
klog.V(1).Info("Claiming to support LoadBalancer")
|
||
|
|
||
|
return &LbaasV2{LoadBalancer{network, compute, lb, os.lbOpts}}, true
|
||
|
}
|
||
|
|
||
|
func isNotFound(err error) bool {
|
||
|
if _, ok := err.(gophercloud.ErrDefault404); ok {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
if errCode, ok := err.(gophercloud.ErrUnexpectedResponseCode); ok {
|
||
|
if errCode.Actual == http.StatusNotFound {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Zones indicates that we support zones
|
||
|
func (os *OpenStack) Zones() (cloudprovider.Zones, bool) {
|
||
|
klog.V(1).Info("Claiming to support Zones")
|
||
|
return os, true
|
||
|
}
|
||
|
|
||
|
// GetZone returns the current zone
|
||
|
func (os *OpenStack) GetZone(ctx context.Context) (cloudprovider.Zone, error) {
|
||
|
md, err := getMetadata(os.metadataOpts.SearchOrder)
|
||
|
if err != nil {
|
||
|
return cloudprovider.Zone{}, err
|
||
|
}
|
||
|
|
||
|
zone := cloudprovider.Zone{
|
||
|
FailureDomain: md.AvailabilityZone,
|
||
|
Region: os.region,
|
||
|
}
|
||
|
klog.V(4).Infof("Current zone is %v", zone)
|
||
|
return zone, nil
|
||
|
}
|
||
|
|
||
|
// GetZoneByProviderID implements Zones.GetZoneByProviderID
|
||
|
// This is particularly useful in external cloud providers where the kubelet
|
||
|
// does not initialize node data.
|
||
|
func (os *OpenStack) GetZoneByProviderID(ctx context.Context, providerID string) (cloudprovider.Zone, error) {
|
||
|
instanceID, err := instanceIDFromProviderID(providerID)
|
||
|
if err != nil {
|
||
|
return cloudprovider.Zone{}, err
|
||
|
}
|
||
|
|
||
|
compute, err := os.NewComputeV2()
|
||
|
if err != nil {
|
||
|
return cloudprovider.Zone{}, err
|
||
|
}
|
||
|
|
||
|
srv, err := servers.Get(compute, instanceID).Extract()
|
||
|
if err != nil {
|
||
|
return cloudprovider.Zone{}, err
|
||
|
}
|
||
|
|
||
|
zone := cloudprovider.Zone{
|
||
|
FailureDomain: srv.Metadata[availabilityZone],
|
||
|
Region: os.region,
|
||
|
}
|
||
|
klog.V(4).Infof("The instance %s in zone %v", srv.Name, zone)
|
||
|
return zone, nil
|
||
|
}
|
||
|
|
||
|
// GetZoneByNodeName implements Zones.GetZoneByNodeName
|
||
|
// This is particularly useful in external cloud providers where the kubelet
|
||
|
// does not initialize node data.
|
||
|
func (os *OpenStack) GetZoneByNodeName(ctx context.Context, nodeName types.NodeName) (cloudprovider.Zone, error) {
|
||
|
compute, err := os.NewComputeV2()
|
||
|
if err != nil {
|
||
|
return cloudprovider.Zone{}, err
|
||
|
}
|
||
|
|
||
|
srv, err := getServerByName(compute, nodeName)
|
||
|
if err != nil {
|
||
|
if err == ErrNotFound {
|
||
|
return cloudprovider.Zone{}, cloudprovider.InstanceNotFound
|
||
|
}
|
||
|
return cloudprovider.Zone{}, err
|
||
|
}
|
||
|
|
||
|
zone := cloudprovider.Zone{
|
||
|
FailureDomain: srv.Metadata[availabilityZone],
|
||
|
Region: os.region,
|
||
|
}
|
||
|
klog.V(4).Infof("The instance %s in zone %v", srv.Name, zone)
|
||
|
return zone, nil
|
||
|
}
|
||
|
|
||
|
// Routes initializes routes support
|
||
|
func (os *OpenStack) Routes() (cloudprovider.Routes, bool) {
|
||
|
klog.V(4).Info("openstack.Routes() called")
|
||
|
|
||
|
network, err := os.NewNetworkV2()
|
||
|
if err != nil {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
netExts, err := networkExtensions(network)
|
||
|
if err != nil {
|
||
|
klog.Warningf("Failed to list neutron extensions: %v", err)
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
if !netExts["extraroute"] {
|
||
|
klog.V(3).Info("Neutron extraroute extension not found, required for Routes support")
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
compute, err := os.NewComputeV2()
|
||
|
if err != nil {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
r, err := NewRoutes(compute, network, os.routeOpts)
|
||
|
if err != nil {
|
||
|
klog.Warningf("Error initialising Routes support: %v", err)
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
klog.V(1).Info("Claiming to support Routes")
|
||
|
return r, true
|
||
|
}
|
||
|
|
||
|
func (os *OpenStack) volumeService(forceVersion string) (volumeService, error) {
|
||
|
bsVersion := ""
|
||
|
if forceVersion == "" {
|
||
|
bsVersion = os.bsOpts.BSVersion
|
||
|
} else {
|
||
|
bsVersion = forceVersion
|
||
|
}
|
||
|
|
||
|
switch bsVersion {
|
||
|
case "v1":
|
||
|
sClient, err := os.NewBlockStorageV1()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
klog.V(3).Info("Using Blockstorage API V1")
|
||
|
return &VolumesV1{sClient, os.bsOpts}, nil
|
||
|
case "v2":
|
||
|
sClient, err := os.NewBlockStorageV2()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
klog.V(3).Info("Using Blockstorage API V2")
|
||
|
return &VolumesV2{sClient, os.bsOpts}, nil
|
||
|
case "v3":
|
||
|
sClient, err := os.NewBlockStorageV3()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
klog.V(3).Info("Using Blockstorage API V3")
|
||
|
return &VolumesV3{sClient, os.bsOpts}, nil
|
||
|
case "auto":
|
||
|
// Currently kubernetes support Cinder v1 / Cinder v2 / Cinder v3.
|
||
|
// Choose Cinder v3 firstly, if kubernetes can't initialize cinder v3 client, try to initialize cinder v2 client.
|
||
|
// If kubernetes can't initialize cinder v2 client, try to initialize cinder v1 client.
|
||
|
// Return appropriate message when kubernetes can't initialize them.
|
||
|
if sClient, err := os.NewBlockStorageV3(); err == nil {
|
||
|
klog.V(3).Info("Using Blockstorage API V3")
|
||
|
return &VolumesV3{sClient, os.bsOpts}, nil
|
||
|
}
|
||
|
|
||
|
if sClient, err := os.NewBlockStorageV2(); err == nil {
|
||
|
klog.V(3).Info("Using Blockstorage API V2")
|
||
|
return &VolumesV2{sClient, os.bsOpts}, nil
|
||
|
}
|
||
|
|
||
|
if sClient, err := os.NewBlockStorageV1(); err == nil {
|
||
|
klog.V(3).Info("Using Blockstorage API V1")
|
||
|
return &VolumesV1{sClient, os.bsOpts}, nil
|
||
|
}
|
||
|
|
||
|
errTxt := "BlockStorage API version autodetection failed. " +
|
||
|
"Please set it explicitly in cloud.conf in section [BlockStorage] with key `bs-version`"
|
||
|
return nil, errors.New(errTxt)
|
||
|
default:
|
||
|
errTxt := fmt.Sprintf("Config error: unrecognised bs-version \"%v\"", os.bsOpts.BSVersion)
|
||
|
return nil, errors.New(errTxt)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func checkMetadataSearchOrder(order string) error {
|
||
|
if order == "" {
|
||
|
return errors.New("invalid value in section [Metadata] with key `search-order`. Value cannot be empty")
|
||
|
}
|
||
|
|
||
|
elements := strings.Split(order, ",")
|
||
|
if len(elements) > 2 {
|
||
|
return errors.New("invalid value in section [Metadata] with key `search-order`. Value cannot contain more than 2 elements")
|
||
|
}
|
||
|
|
||
|
for _, id := range elements {
|
||
|
id = strings.TrimSpace(id)
|
||
|
switch id {
|
||
|
case configDriveID:
|
||
|
case metadataID:
|
||
|
default:
|
||
|
return fmt.Errorf("invalid element %q found in section [Metadata] with key `search-order`."+
|
||
|
"Supported elements include %q and %q", id, configDriveID, metadataID)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|