vendor files

This commit is contained in:
Serguei Bezverkhi
2018-01-09 13:57:14 -05:00
parent 558bc6c02a
commit 7b24313bd6
16547 changed files with 4527373 additions and 0 deletions

View File

@ -0,0 +1,101 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"metadata.go",
"openstack.go",
"openstack_client.go",
"openstack_instances.go",
"openstack_loadbalancer.go",
"openstack_metrics.go",
"openstack_routes.go",
"openstack_volumes.go",
],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/openstack",
deps = [
"//pkg/api/v1/service:go_default_library",
"//pkg/apis/core/v1/helper:go_default_library",
"//pkg/cloudprovider:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/util/mount:go_default_library",
"//pkg/volume:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/pagination:go_default_library",
"//vendor/github.com/mitchellh/mapstructure:go_default_library",
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
"//vendor/gopkg.in/gcfg.v1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/client-go/util/cert:go_default_library",
"//vendor/k8s.io/utils/exec:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"metadata_test.go",
"openstack_routes_test.go",
"openstack_test.go",
],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/openstack",
library = ":go_default_library",
deps = [
"//pkg/cloudprovider:go_default_library",
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/rand:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,6 @@
# Maintainers
* [Angus Lees](https://github.com/anguslees)
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/pkg/cloudprovider/providers/openstack/MAINTAINERS.md?pixel)]()

View File

@ -0,0 +1,10 @@
approvers:
- anguslees
- NickrenREN
- dims
- FengyunPan
reviewers:
- anguslees
- NickrenREN
- dims
- FengyunPan

View File

@ -0,0 +1,197 @@
/*
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 openstack
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/util/mount"
"k8s.io/utils/exec"
)
const (
// metadataUrlTemplate allows building an OpenStack Metadata service URL.
// It's a hardcoded IPv4 link-local address as documented in "OpenStack Cloud
// Administrator Guide", chapter Compute - Networking with nova-network.
// https://docs.openstack.org/admin-guide/compute-networking-nova.html#metadata-service
defaultMetadataVersion = "2012-08-10"
metadataUrlTemplate = "http://169.254.169.254/openstack/%s/meta_data.json"
// metadataID is used as an identifier on the metadata search order configuration.
metadataID = "metadataService"
// Config drive is defined as an iso9660 or vfat (deprecated) drive
// with the "config-2" label.
// http://docs.openstack.org/user-guide/cli-config-drive.html
configDriveLabel = "config-2"
configDrivePathTemplate = "openstack/%s/meta_data.json"
// configDriveID is used as an identifier on the metadata search order configuration.
configDriveID = "configDrive"
)
var ErrBadMetadata = errors.New("invalid OpenStack metadata, got empty uuid")
// There are multiple device types. To keep it simple, we're using a single structure
// for all device metadata types.
type DeviceMetadata struct {
Type string `json:"type"`
Bus string `json:"bus,omitempty"`
Serial string `json:"serial,omitempty"`
Address string `json:"address,omitempty"`
// .. and other fields.
}
// Assumes the "2012-08-10" meta_data.json format.
// See http://docs.openstack.org/user-guide/cli_config_drive.html
type Metadata struct {
Uuid string `json:"uuid"`
Name string `json:"name"`
AvailabilityZone string `json:"availability_zone"`
Devices []DeviceMetadata `json:"devices,omitempty"`
// .. and other fields we don't care about. Expand as necessary.
}
// parseMetadata reads JSON from OpenStack metadata server and parses
// instance ID out of it.
func parseMetadata(r io.Reader) (*Metadata, error) {
var metadata Metadata
json := json.NewDecoder(r)
if err := json.Decode(&metadata); err != nil {
return nil, err
}
if metadata.Uuid == "" {
return nil, ErrBadMetadata
}
return &metadata, nil
}
func getMetadataUrl(metadataVersion string) string {
return fmt.Sprintf(metadataUrlTemplate, metadataVersion)
}
func getConfigDrivePath(metadataVersion string) string {
return fmt.Sprintf(configDrivePathTemplate, metadataVersion)
}
func getMetadataFromConfigDrive(metadataVersion string) (*Metadata, error) {
// Try to read instance UUID from config drive.
dev := "/dev/disk/by-label/" + configDriveLabel
if _, err := os.Stat(dev); os.IsNotExist(err) {
out, err := exec.New().Command(
"blkid", "-l",
"-t", "LABEL="+configDriveLabel,
"-o", "device",
).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("unable to run blkid: %v", err)
}
dev = strings.TrimSpace(string(out))
}
mntdir, err := ioutil.TempDir("", "configdrive")
if err != nil {
return nil, err
}
defer os.Remove(mntdir)
glog.V(4).Infof("Attempting to mount configdrive %s on %s", dev, mntdir)
mounter := mount.New("" /* default mount path */)
err = mounter.Mount(dev, mntdir, "iso9660", []string{"ro"})
if err != nil {
err = mounter.Mount(dev, mntdir, "vfat", []string{"ro"})
}
if err != nil {
return nil, fmt.Errorf("error mounting configdrive %s: %v", dev, err)
}
defer mounter.Unmount(mntdir)
glog.V(4).Infof("Configdrive mounted on %s", mntdir)
configDrivePath := getConfigDrivePath(metadataVersion)
f, err := os.Open(
filepath.Join(mntdir, configDrivePath))
if err != nil {
return nil, fmt.Errorf("error reading %s on config drive: %v", configDrivePath, err)
}
defer f.Close()
return parseMetadata(f)
}
func getMetadataFromMetadataService(metadataVersion string) (*Metadata, error) {
// Try to get JSON from metadata server.
metadataUrl := getMetadataUrl(metadataVersion)
glog.V(4).Infof("Attempting to fetch metadata from %s", metadataUrl)
resp, err := http.Get(metadataUrl)
if err != nil {
return nil, fmt.Errorf("error fetching %s: %v", metadataUrl, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("unexpected status code when reading metadata from %s: %s", metadataUrl, resp.Status)
return nil, err
}
return parseMetadata(resp.Body)
}
// Metadata is fixed for the current host, so cache the value process-wide
var metadataCache *Metadata
func getMetadata(order string) (*Metadata, error) {
if metadataCache == nil {
var md *Metadata
var err error
elements := strings.Split(order, ",")
for _, id := range elements {
id = strings.TrimSpace(id)
switch id {
case configDriveID:
md, err = getMetadataFromConfigDrive(defaultMetadataVersion)
case metadataID:
md, err = getMetadataFromMetadataService(defaultMetadataVersion)
default:
err = fmt.Errorf("%s is not a valid metadata search order option. Supported options are %s and %s", id, configDriveID, metadataID)
}
if err == nil {
break
}
}
if err != nil {
return nil, err
}
metadataCache = md
}
return metadataCache, nil
}

View File

@ -0,0 +1,111 @@
/*
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 openstack
import (
"strings"
"testing"
)
var FakeMetadata = Metadata{
Uuid: "83679162-1378-4288-a2d4-70e13ec132aa",
Name: "test",
AvailabilityZone: "nova",
}
func SetMetadataFixture(value *Metadata) {
metadataCache = value
}
func ClearMetadata() {
metadataCache = nil
}
func TestParseMetadata(t *testing.T) {
_, err := parseMetadata(strings.NewReader("bogus"))
if err == nil {
t.Errorf("Should fail when bad data is provided: %s", err)
}
data := strings.NewReader(`
{
"availability_zone": "nova",
"files": [
{
"content_path": "/content/0000",
"path": "/etc/network/interfaces"
},
{
"content_path": "/content/0001",
"path": "known_hosts"
}
],
"hostname": "test.novalocal",
"launch_index": 0,
"name": "test",
"meta": {
"role": "webservers",
"essential": "false"
},
"public_keys": {
"mykey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDBqUfVvCSez0/Wfpd8dLLgZXV9GtXQ7hnMN+Z0OWQUyebVEHey1CXuin0uY1cAJMhUq8j98SiW+cU0sU4J3x5l2+xi1bodDm1BtFWVeLIOQINpfV1n8fKjHB+ynPpe1F6tMDvrFGUlJs44t30BrujMXBe8Rq44cCk6wqyjATA3rQ== Generated by Nova\n"
},
"uuid": "83679162-1378-4288-a2d4-70e13ec132aa",
"devices": [
{
"bus": "scsi",
"serial": "6df1888b-f373-41cf-b960-3786e60a28ef",
"tags": ["fake_tag"],
"type": "disk",
"address": "0:0:0:0"
}
]
}
`)
md, err := parseMetadata(data)
if err != nil {
t.Fatalf("Should succeed when provided with valid data: %s", err)
}
if md.Name != "test" {
t.Errorf("incorrect name: %s", md.Name)
}
if md.Uuid != "83679162-1378-4288-a2d4-70e13ec132aa" {
t.Errorf("incorrect uuid: %s", md.Uuid)
}
if md.AvailabilityZone != "nova" {
t.Errorf("incorrect az: %s", md.AvailabilityZone)
}
if len(md.Devices) != 1 {
t.Errorf("expecting to find 1 device, found %d", len(md.Devices))
}
if md.Devices[0].Bus != "scsi" {
t.Errorf("incorrect disk bus: %s", md.Devices[0].Bus)
}
if md.Devices[0].Address != "0:0:0:0" {
t.Errorf("incorrect disk address: %s", md.Devices[0].Address)
}
if md.Devices[0].Type != "disk" {
t.Errorf("incorrect device type: %s", md.Devices[0].Type)
}
}

View File

@ -0,0 +1,721 @@
/*
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 (
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"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"
"github.com/golang/glog"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
netutil "k8s.io/apimachinery/pkg/util/net"
certutil "k8s.io/client-go/util/cert"
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
"k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/controller"
)
const (
ProviderName = "openstack"
AvailabilityZone = "availability_zone"
defaultTimeOut = 60 * time.Second
)
var ErrNotFound = errors.New("failed to find object")
var ErrMultipleResults = errors.New("multiple results where only one expected")
var ErrNoAddressFound = errors.New("no address found for host")
// encoding.TextUnmarshaler interface for time.Duration
type MyDuration struct {
time.Duration
}
func (d *MyDuration) UnmarshalText(text []byte) error {
res, err := time.ParseDuration(string(text))
if err != nil {
return err
}
d.Duration = res
return nil
}
type LoadBalancer struct {
network *gophercloud.ServiceClient
compute *gophercloud.ServiceClient
lb *gophercloud.ServiceClient
opts LoadBalancerOpts
}
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
}
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"`
}
type RouterOpts struct {
RouterId string `gcfg:"router-id"` // required
}
type MetadataOpts struct {
SearchOrder string `gcfg:"search-order"`
RequestTimeout MyDuration `gcfg:"request-timeout"`
}
// 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
}
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"`
}
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,
}
}
func readConfig(config io.Reader) (Config, error) {
if config == nil {
return Config{}, fmt.Errorf("no OpenStack cloud provider config file given")
}
var cfg Config
// 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)
return cfg, err
}
// 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)
glog.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")
}
}
if err := checkMetadataSearchOrder(openstackOpts.metadataOpts.SearchOrder); err != nil {
return err
}
return nil
}
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
}
// Initialize passes a Kubernetes clientBuilder interface to the cloud provider
func (os *OpenStack) Initialize(clientBuilder controller.ControllerClientBuilder) {}
// 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)
}
// 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))),
Status: "ACTIVE",
}
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
}
v1helper.AddToNodeAddresses(&addrs,
v1.NodeAddress{
Type: addressType,
Address: props.Addr,
},
)
}
}
// AccessIPs are usually duplicates of "public" addresses.
if srv.AccessIPv4 != "" {
v1helper.AddToNodeAddresses(&addrs,
v1.NodeAddress{
Type: v1.NodeExternalIP,
Address: srv.AccessIPv4,
},
)
}
if srv.AccessIPv6 != "" {
v1helper.AddToNodeAddresses(&addrs,
v1.NodeAddress{
Type: v1.NodeExternalIP,
Address: srv.AccessIPv6,
},
)
}
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) (string, error) {
addrs, err := getAddressesByName(client, name)
if err != nil {
return "", err
} else if len(addrs) == 0 {
return "", ErrNoAddressFound
}
for _, addr := range addrs {
if addr.Type == v1.NodeInternalIP {
return addr.Address, nil
}
}
return addrs[0].Address, nil
}
// 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
}
func (os *OpenStack) Clusters() (cloudprovider.Clusters, bool) {
return nil, false
}
// ProviderName returns the cloud provider ID.
func (os *OpenStack) ProviderName() string {
return ProviderName
}
// ScrubDNS filters DNS settings for pods.
func (os *OpenStack) ScrubDNS(nameServers, searches []string) ([]string, []string) {
return nameServers, searches
}
// HasClusterID returns true if the cluster has a clusterID
func (os *OpenStack) HasClusterID() bool {
return true
}
func (os *OpenStack) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
glog.V(4).Info("openstack.LoadBalancer() called")
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" {
glog.Warningf("Config error: currently only support LBaaS v2, unrecognised lb-version \"%v\"", lbVersion)
return nil, false
}
glog.V(1).Info("Claiming to support LoadBalancer")
return &LbaasV2{LoadBalancer{network, compute, lb, os.lbOpts}}, true
}
func isNotFound(err error) bool {
e, ok := err.(*gophercloud.ErrUnexpectedResponseCode)
return ok && e.Actual == http.StatusNotFound
}
func (os *OpenStack) Zones() (cloudprovider.Zones, bool) {
glog.V(1).Info("Claiming to support Zones")
return os, true
}
func (os *OpenStack) GetZone() (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,
}
glog.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(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,
}
glog.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(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,
}
glog.V(4).Infof("The instance %s in zone %v", srv.Name, zone)
return zone, nil
}
func (os *OpenStack) Routes() (cloudprovider.Routes, bool) {
glog.V(4).Info("openstack.Routes() called")
network, err := os.NewNetworkV2()
if err != nil {
return nil, false
}
netExts, err := networkExtensions(network)
if err != nil {
glog.Warningf("Failed to list neutron extensions: %v", err)
return nil, false
}
if !netExts["extraroute"] {
glog.V(3).Infof("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 {
glog.Warningf("Error initialising Routes support: %v", err)
return nil, false
}
glog.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
}
glog.V(3).Infof("Using Blockstorage API V1")
return &VolumesV1{sClient, os.bsOpts}, nil
case "v2":
sClient, err := os.NewBlockStorageV2()
if err != nil {
return nil, err
}
glog.V(3).Infof("Using Blockstorage API V2")
return &VolumesV2{sClient, os.bsOpts}, nil
case "v3":
sClient, err := os.NewBlockStorageV3()
if err != nil {
return nil, err
}
glog.V(3).Infof("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 {
glog.V(3).Infof("Using Blockstorage API V3")
return &VolumesV3{sClient, os.bsOpts}, nil
}
if sClient, err := os.NewBlockStorageV2(); err == nil {
glog.V(3).Infof("Using Blockstorage API V2")
return &VolumesV2{sClient, os.bsOpts}, nil
}
if sClient, err := os.NewBlockStorageV1(); err == nil {
glog.V(3).Infof("Using Blockstorage API V1")
return &VolumesV1{sClient, os.bsOpts}, nil
}
err_txt := "BlockStorage API version autodetection failed. " +
"Please set it explicitly in cloud.conf in section [BlockStorage] with key `bs-version`"
return nil, errors.New(err_txt)
default:
err_txt := fmt.Sprintf("Config error: unrecognised bs-version \"%v\"", os.bsOpts.BSVersion)
return nil, errors.New(err_txt)
}
}
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
}

View File

@ -0,0 +1,92 @@
/*
Copyright 2017 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 (
"fmt"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
)
func (os *OpenStack) NewNetworkV2() (*gophercloud.ServiceClient, error) {
network, err := openstack.NewNetworkV2(os.provider, gophercloud.EndpointOpts{
Region: os.region,
})
if err != nil {
return nil, fmt.Errorf("failed to find network v2 endpoint for region %s: %v", os.region, err)
}
return network, nil
}
func (os *OpenStack) NewComputeV2() (*gophercloud.ServiceClient, error) {
compute, err := openstack.NewComputeV2(os.provider, gophercloud.EndpointOpts{
Region: os.region,
})
if err != nil {
return nil, fmt.Errorf("failed to find compute v2 endpoint for region %s: %v", os.region, err)
}
return compute, nil
}
func (os *OpenStack) NewBlockStorageV1() (*gophercloud.ServiceClient, error) {
storage, err := openstack.NewBlockStorageV1(os.provider, gophercloud.EndpointOpts{
Region: os.region,
})
if err != nil {
return nil, fmt.Errorf("unable to initialize cinder v1 client for region %s: %v", os.region, err)
}
return storage, nil
}
func (os *OpenStack) NewBlockStorageV2() (*gophercloud.ServiceClient, error) {
storage, err := openstack.NewBlockStorageV2(os.provider, gophercloud.EndpointOpts{
Region: os.region,
})
if err != nil {
return nil, fmt.Errorf("unable to initialize cinder v2 client for region %s: %v", os.region, err)
}
return storage, nil
}
func (os *OpenStack) NewBlockStorageV3() (*gophercloud.ServiceClient, error) {
storage, err := openstack.NewBlockStorageV3(os.provider, gophercloud.EndpointOpts{
Region: os.region,
})
if err != nil {
return nil, fmt.Errorf("unable to initialize cinder v3 client for region %s: %v", os.region, err)
}
return storage, nil
}
func (os *OpenStack) NewLoadBalancerV2() (*gophercloud.ServiceClient, error) {
var lb *gophercloud.ServiceClient
var err error
if os.lbOpts.UseOctavia {
lb, err = openstack.NewLoadBalancerV2(os.provider, gophercloud.EndpointOpts{
Region: os.region,
})
} else {
lb, err = openstack.NewNetworkV2(os.provider, gophercloud.EndpointOpts{
Region: os.region,
})
}
if err != nil {
return nil, fmt.Errorf("failed to find load-balancer v2 endpoint for region %s: %v", os.region, err)
}
return lb, nil
}

View File

@ -0,0 +1,222 @@
/*
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 openstack
import (
"fmt"
"regexp"
"github.com/golang/glog"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/pkg/cloudprovider"
)
type Instances struct {
compute *gophercloud.ServiceClient
opts MetadataOpts
}
// Instances returns an implementation of Instances for OpenStack.
func (os *OpenStack) Instances() (cloudprovider.Instances, bool) {
glog.V(4).Info("openstack.Instances() called")
compute, err := os.NewComputeV2()
if err != nil {
return nil, false
}
glog.V(1).Info("Claiming to support Instances")
return &Instances{
compute: compute,
opts: os.metadataOpts,
}, true
}
// Implementation of Instances.CurrentNodeName
// Note this is *not* necessarily the same as hostname.
func (i *Instances) CurrentNodeName(hostname string) (types.NodeName, error) {
md, err := getMetadata(i.opts.SearchOrder)
if err != nil {
return "", err
}
return types.NodeName(md.Name), nil
}
func (i *Instances) AddSSHKeyToAllInstances(user string, keyData []byte) error {
return cloudprovider.NotImplemented
}
func (i *Instances) NodeAddresses(name types.NodeName) ([]v1.NodeAddress, error) {
glog.V(4).Infof("NodeAddresses(%v) called", name)
addrs, err := getAddressesByName(i.compute, name)
if err != nil {
return nil, err
}
glog.V(4).Infof("NodeAddresses(%v) => %v", name, addrs)
return addrs, nil
}
// NodeAddressesByProviderID returns the node addresses of an instances with the specified unique providerID
// This method will not be called from the node that is requesting this ID. i.e. metadata service
// and other local methods cannot be used here
func (i *Instances) NodeAddressesByProviderID(providerID string) ([]v1.NodeAddress, error) {
instanceID, err := instanceIDFromProviderID(providerID)
if err != nil {
return []v1.NodeAddress{}, err
}
server, err := servers.Get(i.compute, instanceID).Extract()
if err != nil {
return []v1.NodeAddress{}, err
}
addresses, err := nodeAddresses(server)
if err != nil {
return []v1.NodeAddress{}, err
}
return addresses, nil
}
// ExternalID returns the cloud provider ID of the specified instance (deprecated).
func (i *Instances) ExternalID(name types.NodeName) (string, error) {
srv, err := getServerByName(i.compute, name)
if err != nil {
if err == ErrNotFound {
return "", cloudprovider.InstanceNotFound
}
return "", err
}
return srv.ID, nil
}
// InstanceExistsByProviderID returns true if the instance with the given provider id still exists and is running.
// If false is returned with no error, the instance will be immediately deleted by the cloud controller manager.
func (i *Instances) InstanceExistsByProviderID(providerID string) (bool, error) {
instanceID, err := instanceIDFromProviderID(providerID)
if err != nil {
return false, err
}
server, err := servers.Get(i.compute, instanceID).Extract()
if err != nil {
if isNotFound(err) {
return false, nil
}
return false, err
}
if server.Status != "ACTIVE" {
glog.Warningf("the instance %s is not active", instanceID)
return false, nil
}
return true, nil
}
// InstanceID returns the kubelet's cloud provider ID.
func (os *OpenStack) InstanceID() (string, error) {
if len(os.localInstanceID) == 0 {
id, err := readInstanceID(os.metadataOpts.SearchOrder)
if err != nil {
return "", err
}
os.localInstanceID = id
}
return os.localInstanceID, nil
}
// InstanceID returns the cloud provider ID of the specified instance.
func (i *Instances) InstanceID(name types.NodeName) (string, error) {
srv, err := getServerByName(i.compute, name)
if err != nil {
if err == ErrNotFound {
return "", cloudprovider.InstanceNotFound
}
return "", err
}
// In the future it is possible to also return an endpoint as:
// <endpoint>/<instanceid>
return "/" + srv.ID, nil
}
// InstanceTypeByProviderID returns the cloudprovider instance type of the node with the specified unique providerID
// This method will not be called from the node that is requesting this ID. i.e. metadata service
// and other local methods cannot be used here
func (i *Instances) InstanceTypeByProviderID(providerID string) (string, error) {
instanceID, err := instanceIDFromProviderID(providerID)
if err != nil {
return "", err
}
server, err := servers.Get(i.compute, instanceID).Extract()
if err != nil {
return "", err
}
return srvInstanceType(server)
}
// InstanceType returns the type of the specified instance.
func (i *Instances) InstanceType(name types.NodeName) (string, error) {
srv, err := getServerByName(i.compute, name)
if err != nil {
return "", err
}
return srvInstanceType(srv)
}
func srvInstanceType(srv *servers.Server) (string, error) {
keys := []string{"name", "id", "original_name"}
for _, key := range keys {
val, found := srv.Flavor[key]
if found {
flavor, ok := val.(string)
if ok {
return flavor, nil
}
}
}
return "", fmt.Errorf("flavor name/id not found")
}
// instanceIDFromProviderID splits a provider's id and return instanceID.
// A providerID is build out of '${ProviderName}:///${instance-id}'which contains ':///'.
// See cloudprovider.GetInstanceProviderID and Instances.InstanceID.
func instanceIDFromProviderID(providerID string) (instanceID string, err error) {
// If Instances.InstanceID or cloudprovider.GetInstanceProviderID is changed, the regexp should be changed too.
var providerIdRegexp = regexp.MustCompile(`^` + ProviderName + `:///([^/]+)$`)
matches := providerIdRegexp.FindStringSubmatch(providerID)
if len(matches) != 2 {
return "", fmt.Errorf("ProviderID \"%s\" didn't match expected format \"openstack:///InstanceID\"", providerID)
}
return matches[1], nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
/*
Copyright 2017 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 "github.com/prometheus/client_golang/prometheus"
const (
OpenstackSubsystem = "openstack"
OpenstackOperationKey = "cloudprovider_openstack_api_request_duration_seconds"
OpenstackOperationErrorKey = "cloudprovider_openstack_api_request_errors"
)
var (
OpenstackOperationsLatency = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Subsystem: OpenstackSubsystem,
Name: OpenstackOperationKey,
Help: "Latency of openstack api call",
},
[]string{"request"},
)
OpenstackApiRequestErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Subsystem: OpenstackSubsystem,
Name: OpenstackOperationErrorKey,
Help: "Cumulative number of openstack Api call errors",
},
[]string{"request"},
)
)
func RegisterMetrics() {
prometheus.MustRegister(OpenstackOperationsLatency)
prometheus.MustRegister(OpenstackApiRequestErrors)
}

View File

@ -0,0 +1,323 @@
/*
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 openstack
import (
"errors"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers"
neutronports "github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/pkg/cloudprovider"
)
var ErrNoRouterId = errors.New("router-id not set in cloud provider config")
type Routes struct {
compute *gophercloud.ServiceClient
network *gophercloud.ServiceClient
opts RouterOpts
}
func NewRoutes(compute *gophercloud.ServiceClient, network *gophercloud.ServiceClient, opts RouterOpts) (cloudprovider.Routes, error) {
if opts.RouterId == "" {
return nil, ErrNoRouterId
}
return &Routes{
compute: compute,
network: network,
opts: opts,
}, nil
}
func (r *Routes) ListRoutes(clusterName string) ([]*cloudprovider.Route, error) {
glog.V(4).Infof("ListRoutes(%v)", clusterName)
nodeNamesByAddr := make(map[string]types.NodeName)
err := foreachServer(r.compute, servers.ListOpts{Status: "ACTIVE"}, func(srv *servers.Server) (bool, error) {
addrs, err := nodeAddresses(srv)
if err != nil {
return false, err
}
name := mapServerToNodeName(srv)
for _, addr := range addrs {
nodeNamesByAddr[addr.Address] = name
}
return true, nil
})
if err != nil {
return nil, err
}
router, err := routers.Get(r.network, r.opts.RouterId).Extract()
if err != nil {
return nil, err
}
var routes []*cloudprovider.Route
for _, item := range router.Routes {
nodeName, ok := nodeNamesByAddr[item.NextHop]
if !ok {
// Not one of our routes?
glog.V(4).Infof("Skipping route with unknown nexthop %v", item.NextHop)
continue
}
route := cloudprovider.Route{
Name: item.DestinationCIDR,
TargetNode: nodeName,
DestinationCIDR: item.DestinationCIDR,
}
routes = append(routes, &route)
}
return routes, nil
}
func updateRoutes(network *gophercloud.ServiceClient, router *routers.Router, newRoutes []routers.Route) (func(), error) {
origRoutes := router.Routes // shallow copy
_, err := routers.Update(network, router.ID, routers.UpdateOpts{
Routes: newRoutes,
}).Extract()
if err != nil {
return nil, err
}
unwinder := func() {
glog.V(4).Info("Reverting routes change to router ", router.ID)
_, err := routers.Update(network, router.ID, routers.UpdateOpts{
Routes: origRoutes,
}).Extract()
if err != nil {
glog.Warning("Unable to reset routes during error unwind: ", err)
}
}
return unwinder, nil
}
func updateAllowedAddressPairs(network *gophercloud.ServiceClient, port *neutronports.Port, newPairs []neutronports.AddressPair) (func(), error) {
origPairs := port.AllowedAddressPairs // shallow copy
_, err := neutronports.Update(network, port.ID, neutronports.UpdateOpts{
AllowedAddressPairs: &newPairs,
}).Extract()
if err != nil {
return nil, err
}
unwinder := func() {
glog.V(4).Info("Reverting allowed-address-pairs change to port ", port.ID)
_, err := neutronports.Update(network, port.ID, neutronports.UpdateOpts{
AllowedAddressPairs: &origPairs,
}).Extract()
if err != nil {
glog.Warning("Unable to reset allowed-address-pairs during error unwind: ", err)
}
}
return unwinder, nil
}
func (r *Routes) CreateRoute(clusterName string, nameHint string, route *cloudprovider.Route) error {
glog.V(4).Infof("CreateRoute(%v, %v, %v)", clusterName, nameHint, route)
onFailure := NewCaller()
addr, err := getAddressByName(r.compute, route.TargetNode)
if err != nil {
return err
}
glog.V(4).Infof("Using nexthop %v for node %v", addr, route.TargetNode)
router, err := routers.Get(r.network, r.opts.RouterId).Extract()
if err != nil {
return err
}
routes := router.Routes
for _, item := range routes {
if item.DestinationCIDR == route.DestinationCIDR && item.NextHop == addr {
glog.V(4).Infof("Skipping existing route: %v", route)
return nil
}
}
routes = append(routes, routers.Route{
DestinationCIDR: route.DestinationCIDR,
NextHop: addr,
})
unwind, err := updateRoutes(r.network, router, routes)
if err != nil {
return err
}
defer onFailure.Call(unwind)
// get the port of addr on target node.
portID, err := getPortIDByIP(r.compute, route.TargetNode, addr)
if err != nil {
return err
}
port, err := getPortByID(r.network, portID)
if err != nil {
return err
}
found := false
for _, item := range port.AllowedAddressPairs {
if item.IPAddress == route.DestinationCIDR {
glog.V(4).Info("Found existing allowed-address-pair: ", item)
found = true
break
}
}
if !found {
newPairs := append(port.AllowedAddressPairs, neutronports.AddressPair{
IPAddress: route.DestinationCIDR,
})
unwind, err := updateAllowedAddressPairs(r.network, port, newPairs)
if err != nil {
return err
}
defer onFailure.Call(unwind)
}
glog.V(4).Infof("Route created: %v", route)
onFailure.Disarm()
return nil
}
func (r *Routes) DeleteRoute(clusterName string, route *cloudprovider.Route) error {
glog.V(4).Infof("DeleteRoute(%v, %v)", clusterName, route)
onFailure := NewCaller()
addr, err := getAddressByName(r.compute, route.TargetNode)
if err != nil {
return err
}
router, err := routers.Get(r.network, r.opts.RouterId).Extract()
if err != nil {
return err
}
routes := router.Routes
index := -1
for i, item := range routes {
if item.DestinationCIDR == route.DestinationCIDR && item.NextHop == addr {
index = i
break
}
}
if index == -1 {
glog.V(4).Infof("Skipping non-existent route: %v", route)
return nil
}
// Delete element `index`
routes[index] = routes[len(routes)-1]
routes = routes[:len(routes)-1]
unwind, err := updateRoutes(r.network, router, routes)
if err != nil {
return err
}
defer onFailure.Call(unwind)
// get the port of addr on target node.
portID, err := getPortIDByIP(r.compute, route.TargetNode, addr)
if err != nil {
return err
}
port, err := getPortByID(r.network, portID)
if err != nil {
return err
}
addr_pairs := port.AllowedAddressPairs
index = -1
for i, item := range addr_pairs {
if item.IPAddress == route.DestinationCIDR {
index = i
break
}
}
if index != -1 {
// Delete element `index`
addr_pairs[index] = addr_pairs[len(addr_pairs)-1]
addr_pairs = addr_pairs[:len(addr_pairs)-1]
unwind, err := updateAllowedAddressPairs(r.network, port, addr_pairs)
if err != nil {
return err
}
defer onFailure.Call(unwind)
}
glog.V(4).Infof("Route deleted: %v", route)
onFailure.Disarm()
return nil
}
func getPortIDByIP(compute *gophercloud.ServiceClient, targetNode types.NodeName, ipAddress string) (string, error) {
srv, err := getServerByName(compute, targetNode)
if err != nil {
return "", err
}
interfaces, err := getAttachedInterfacesByID(compute, srv.ID)
if err != nil {
return "", err
}
for _, intf := range interfaces {
for _, fixedIP := range intf.FixedIPs {
if fixedIP.IPAddress == ipAddress {
return intf.PortID, nil
}
}
}
return "", ErrNotFound
}
func getPortByID(client *gophercloud.ServiceClient, portID string) (*neutronports.Port, error) {
targetPort, err := neutronports.Get(client, portID).Extract()
if err != nil {
return nil, err
}
if targetPort == nil {
return nil, ErrNotFound
}
return targetPort, nil
}

View File

@ -0,0 +1,113 @@
/*
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 openstack
import (
"net"
"testing"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/pkg/cloudprovider"
)
func TestRoutes(t *testing.T) {
const clusterName = "ignored"
cfg, ok := configFromEnv()
if !ok {
t.Skipf("No config found in environment")
}
os, err := newOpenStack(cfg)
if err != nil {
t.Fatalf("Failed to construct/authenticate OpenStack: %s", err)
}
// Pick the first router and server to try a test with
os.routeOpts.RouterId = getRouters(os)[0].ID
servername := getServers(os)[0].Name
r, ok := os.Routes()
if !ok {
t.Skip("Routes() returned false - perhaps your stack does not support Neutron extraroute extension?")
}
newroute := cloudprovider.Route{
DestinationCIDR: "10.164.2.0/24",
TargetNode: types.NodeName(servername),
}
err = r.CreateRoute(clusterName, "myhint", &newroute)
if err != nil {
t.Fatalf("CreateRoute error: %v", err)
}
routelist, err := r.ListRoutes(clusterName)
if err != nil {
t.Fatalf("ListRoutes() error: %v", err)
}
for _, route := range routelist {
_, cidr, err := net.ParseCIDR(route.DestinationCIDR)
if err != nil {
t.Logf("Ignoring route %s, unparsable CIDR: %v", route.Name, err)
continue
}
t.Logf("%s via %s", cidr, route.TargetNode)
}
err = r.DeleteRoute(clusterName, &newroute)
if err != nil {
t.Fatalf("DeleteRoute error: %v", err)
}
}
func getServers(os *OpenStack) []servers.Server {
c, err := os.NewComputeV2()
allPages, err := servers.List(c, servers.ListOpts{}).AllPages()
if err != nil {
panic(err)
}
allServers, err := servers.ExtractServers(allPages)
if err != nil {
panic(err)
}
if len(allServers) == 0 {
panic("No servers to test with")
}
return allServers
}
func getRouters(os *OpenStack) []routers.Router {
listOpts := routers.ListOpts{}
n, err := os.NewNetworkV2()
if err != nil {
panic(err)
}
allPages, err := routers.List(n, listOpts).AllPages()
if err != nil {
panic(err)
}
allRouters, err := routers.ExtractRouters(allPages)
if err != nil {
panic(err)
}
if len(allRouters) == 0 {
panic("No routers to test with")
}
return allRouters
}

View File

@ -0,0 +1,604 @@
/*
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 (
"fmt"
"os"
"reflect"
"regexp"
"sort"
"strings"
"testing"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/apimachinery/pkg/util/wait"
)
const (
volumeAvailableStatus = "available"
volumeInUseStatus = "in-use"
testClusterName = "testCluster"
volumeStatusTimeoutSeconds = 30
// volumeStatus* is configuration of exponential backoff for
// waiting for specified volume status. Starting with 1
// seconds, multiplying by 1.2 with each step and taking 13 steps at maximum
// it will time out after 32s, which roughly corresponds to 30s
volumeStatusInitDealy = 1 * time.Second
volumeStatusFactor = 1.2
volumeStatusSteps = 13
)
func WaitForVolumeStatus(t *testing.T, os *OpenStack, volumeName string, status string) {
backoff := wait.Backoff{
Duration: volumeStatusInitDealy,
Factor: volumeStatusFactor,
Steps: volumeStatusSteps,
}
err := wait.ExponentialBackoff(backoff, func() (bool, error) {
getVol, err := os.getVolume(volumeName)
if err != nil {
return false, err
}
if getVol.Status == status {
t.Logf("Volume (%s) status changed to %s after %v seconds\n",
volumeName,
status,
volumeStatusTimeoutSeconds)
return true, nil
} else {
return false, nil
}
})
if err == wait.ErrWaitTimeout {
t.Logf("Volume (%s) status did not change to %s after %v seconds\n",
volumeName,
status,
volumeStatusTimeoutSeconds)
return
}
if err != nil {
t.Fatalf("Cannot get existing Cinder volume (%s): %v", volumeName, err)
}
}
func TestReadConfig(t *testing.T) {
_, err := readConfig(nil)
if err == nil {
t.Errorf("Should fail when no config is provided: %s", err)
}
cfg, err := readConfig(strings.NewReader(`
[Global]
auth-url = http://auth.url
username = user
[LoadBalancer]
create-monitor = yes
monitor-delay = 1m
monitor-timeout = 30s
monitor-max-retries = 3
[BlockStorage]
bs-version = auto
trust-device-path = yes
ignore-volume-az = yes
[Metadata]
search-order = configDrive, metadataService
`))
if err != nil {
t.Fatalf("Should succeed when a valid config is provided: %s", err)
}
if cfg.Global.AuthUrl != "http://auth.url" {
t.Errorf("incorrect authurl: %s", cfg.Global.AuthUrl)
}
if !cfg.LoadBalancer.CreateMonitor {
t.Errorf("incorrect lb.createmonitor: %t", cfg.LoadBalancer.CreateMonitor)
}
if cfg.LoadBalancer.MonitorDelay.Duration != 1*time.Minute {
t.Errorf("incorrect lb.monitordelay: %s", cfg.LoadBalancer.MonitorDelay)
}
if cfg.LoadBalancer.MonitorTimeout.Duration != 30*time.Second {
t.Errorf("incorrect lb.monitortimeout: %s", cfg.LoadBalancer.MonitorTimeout)
}
if cfg.LoadBalancer.MonitorMaxRetries != 3 {
t.Errorf("incorrect lb.monitormaxretries: %d", cfg.LoadBalancer.MonitorMaxRetries)
}
if cfg.BlockStorage.TrustDevicePath != true {
t.Errorf("incorrect bs.trustdevicepath: %v", cfg.BlockStorage.TrustDevicePath)
}
if cfg.BlockStorage.BSVersion != "auto" {
t.Errorf("incorrect bs.bs-version: %v", cfg.BlockStorage.BSVersion)
}
if cfg.BlockStorage.IgnoreVolumeAZ != true {
t.Errorf("incorrect bs.IgnoreVolumeAZ: %v", cfg.BlockStorage.IgnoreVolumeAZ)
}
if cfg.Metadata.SearchOrder != "configDrive, metadataService" {
t.Errorf("incorrect md.search-order: %v", cfg.Metadata.SearchOrder)
}
}
func TestToAuthOptions(t *testing.T) {
cfg := Config{}
cfg.Global.Username = "user"
// etc.
ao := cfg.toAuthOptions()
if !ao.AllowReauth {
t.Errorf("Will need to be able to reauthenticate")
}
if ao.Username != cfg.Global.Username {
t.Errorf("Username %s != %s", ao.Username, cfg.Global.Username)
}
}
func TestCheckOpenStackOpts(t *testing.T) {
delay := MyDuration{60 * time.Second}
timeout := MyDuration{30 * time.Second}
tests := []struct {
name string
openstackOpts *OpenStack
expectedError error
}{
{
name: "test1",
openstackOpts: &OpenStack{
provider: nil,
lbOpts: LoadBalancerOpts{
LBVersion: "v2",
SubnetId: "6261548e-ffde-4bc7-bd22-59c83578c5ef",
FloatingNetworkId: "38b8b5f9-64dc-4424-bf86-679595714786",
LBMethod: "ROUND_ROBIN",
LBProvider: "haproxy",
CreateMonitor: true,
MonitorDelay: delay,
MonitorTimeout: timeout,
MonitorMaxRetries: uint(3),
ManageSecurityGroups: true,
},
metadataOpts: MetadataOpts{
SearchOrder: configDriveID,
},
},
expectedError: nil,
},
{
name: "test2",
openstackOpts: &OpenStack{
provider: nil,
lbOpts: LoadBalancerOpts{
LBVersion: "v2",
FloatingNetworkId: "38b8b5f9-64dc-4424-bf86-679595714786",
LBMethod: "ROUND_ROBIN",
CreateMonitor: true,
MonitorDelay: delay,
MonitorTimeout: timeout,
MonitorMaxRetries: uint(3),
ManageSecurityGroups: true,
},
metadataOpts: MetadataOpts{
SearchOrder: configDriveID,
},
},
expectedError: nil,
},
{
name: "test3",
openstackOpts: &OpenStack{
provider: nil,
lbOpts: LoadBalancerOpts{
LBVersion: "v2",
SubnetId: "6261548e-ffde-4bc7-bd22-59c83578c5ef",
FloatingNetworkId: "38b8b5f9-64dc-4424-bf86-679595714786",
LBMethod: "ROUND_ROBIN",
CreateMonitor: true,
ManageSecurityGroups: true,
},
metadataOpts: MetadataOpts{
SearchOrder: configDriveID,
},
},
expectedError: fmt.Errorf("monitor-delay not set in cloud provider config"),
},
{
name: "test4",
openstackOpts: &OpenStack{
provider: nil,
metadataOpts: MetadataOpts{
SearchOrder: "",
},
},
expectedError: fmt.Errorf("invalid value in section [Metadata] with key `search-order`. Value cannot be empty"),
},
{
name: "test5",
openstackOpts: &OpenStack{
provider: nil,
metadataOpts: MetadataOpts{
SearchOrder: "value1,value2,value3",
},
},
expectedError: fmt.Errorf("invalid value in section [Metadata] with key `search-order`. Value cannot contain more than 2 elements"),
},
{
name: "test6",
openstackOpts: &OpenStack{
provider: nil,
metadataOpts: MetadataOpts{
SearchOrder: "value1",
},
},
expectedError: fmt.Errorf("invalid element %q found in section [Metadata] with key `search-order`."+
"Supported elements include %q and %q", "value1", configDriveID, metadataID),
},
}
for _, testcase := range tests {
err := checkOpenStackOpts(testcase.openstackOpts)
if err == nil && testcase.expectedError == nil {
continue
}
if (err != nil && testcase.expectedError == nil) || (err == nil && testcase.expectedError != nil) || err.Error() != testcase.expectedError.Error() {
t.Errorf("%s failed: expected err=%q, got %q",
testcase.name, testcase.expectedError, err)
}
}
}
func TestCaller(t *testing.T) {
called := false
myFunc := func() { called = true }
c := NewCaller()
c.Call(myFunc)
if !called {
t.Errorf("Caller failed to call function in default case")
}
c.Disarm()
called = false
c.Call(myFunc)
if called {
t.Error("Caller still called function when disarmed")
}
// Confirm the "usual" deferred Caller pattern works as expected
called = false
success_case := func() {
c := NewCaller()
defer c.Call(func() { called = true })
c.Disarm()
}
if success_case(); called {
t.Error("Deferred success case still invoked unwind")
}
called = false
failure_case := func() {
c := NewCaller()
defer c.Call(func() { called = true })
}
if failure_case(); !called {
t.Error("Deferred failure case failed to invoke unwind")
}
}
// An arbitrary sort.Interface, just for easier comparison
type AddressSlice []v1.NodeAddress
func (a AddressSlice) Len() int { return len(a) }
func (a AddressSlice) Less(i, j int) bool { return a[i].Address < a[j].Address }
func (a AddressSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func TestNodeAddresses(t *testing.T) {
srv := servers.Server{
Status: "ACTIVE",
HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
AccessIPv4: "50.56.176.99",
AccessIPv6: "2001:4800:790e:510:be76:4eff:fe04:82a8",
Addresses: map[string]interface{}{
"private": []interface{}{
map[string]interface{}{
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
"version": float64(4),
"addr": "10.0.0.32",
"OS-EXT-IPS:type": "fixed",
},
map[string]interface{}{
"version": float64(4),
"addr": "50.56.176.36",
"OS-EXT-IPS:type": "floating",
},
map[string]interface{}{
"version": float64(4),
"addr": "10.0.0.31",
// No OS-EXT-IPS:type
},
},
"public": []interface{}{
map[string]interface{}{
"version": float64(4),
"addr": "50.56.176.35",
},
map[string]interface{}{
"version": float64(6),
"addr": "2001:4800:780e:510:be76:4eff:fe04:84a8",
},
},
},
}
addrs, err := nodeAddresses(&srv)
if err != nil {
t.Fatalf("nodeAddresses returned error: %v", err)
}
sort.Sort(AddressSlice(addrs))
t.Logf("addresses is %v", addrs)
want := []v1.NodeAddress{
{Type: v1.NodeInternalIP, Address: "10.0.0.31"},
{Type: v1.NodeInternalIP, Address: "10.0.0.32"},
{Type: v1.NodeExternalIP, Address: "2001:4800:780e:510:be76:4eff:fe04:84a8"},
{Type: v1.NodeExternalIP, Address: "2001:4800:790e:510:be76:4eff:fe04:82a8"},
{Type: v1.NodeExternalIP, Address: "50.56.176.35"},
{Type: v1.NodeExternalIP, Address: "50.56.176.36"},
{Type: v1.NodeExternalIP, Address: "50.56.176.99"},
}
if !reflect.DeepEqual(want, addrs) {
t.Errorf("nodeAddresses returned incorrect value %v", addrs)
}
}
// This allows acceptance testing against an existing OpenStack
// install, using the standard OS_* OpenStack client environment
// variables.
// FIXME: it would be better to hermetically test against canned JSON
// requests/responses.
func configFromEnv() (cfg Config, ok bool) {
cfg.Global.AuthUrl = os.Getenv("OS_AUTH_URL")
cfg.Global.TenantId = os.Getenv("OS_TENANT_ID")
// Rax/nova _insists_ that we don't specify both tenant ID and name
if cfg.Global.TenantId == "" {
cfg.Global.TenantName = os.Getenv("OS_TENANT_NAME")
}
cfg.Global.Username = os.Getenv("OS_USERNAME")
cfg.Global.Password = os.Getenv("OS_PASSWORD")
cfg.Global.Region = os.Getenv("OS_REGION_NAME")
cfg.Global.TenantName = os.Getenv("OS_TENANT_NAME")
if cfg.Global.TenantName == "" {
cfg.Global.TenantName = os.Getenv("OS_PROJECT_NAME")
}
cfg.Global.TenantId = os.Getenv("OS_TENANT_ID")
if cfg.Global.TenantId == "" {
cfg.Global.TenantId = os.Getenv("OS_PROJECT_ID")
}
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")
}
ok = (cfg.Global.AuthUrl != "" &&
cfg.Global.Username != "" &&
cfg.Global.Password != "" &&
(cfg.Global.TenantId != "" || cfg.Global.TenantName != "" ||
cfg.Global.DomainId != "" || cfg.Global.DomainName != ""))
cfg.Metadata.SearchOrder = fmt.Sprintf("%s,%s", configDriveID, metadataID)
cfg.BlockStorage.BSVersion = "auto"
return
}
func TestNewOpenStack(t *testing.T) {
cfg, ok := configFromEnv()
if !ok {
t.Skipf("No config found in environment")
}
_, err := newOpenStack(cfg)
if err != nil {
t.Fatalf("Failed to construct/authenticate OpenStack: %s", err)
}
}
func TestLoadBalancer(t *testing.T) {
cfg, ok := configFromEnv()
if !ok {
t.Skipf("No config found in environment")
}
versions := []string{"v2", ""}
for _, v := range versions {
t.Logf("Trying LBVersion = '%s'\n", v)
cfg.LoadBalancer.LBVersion = v
os, err := newOpenStack(cfg)
if err != nil {
t.Fatalf("Failed to construct/authenticate OpenStack: %s", err)
}
lb, ok := os.LoadBalancer()
if !ok {
t.Fatalf("LoadBalancer() returned false - perhaps your stack doesn't support Neutron?")
}
_, exists, err := lb.GetLoadBalancer(testClusterName, &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "noexist"}})
if err != nil {
t.Fatalf("GetLoadBalancer(\"noexist\") returned error: %s", err)
}
if exists {
t.Fatalf("GetLoadBalancer(\"noexist\") returned exists")
}
}
}
func TestZones(t *testing.T) {
SetMetadataFixture(&FakeMetadata)
defer ClearMetadata()
os := OpenStack{
provider: &gophercloud.ProviderClient{
IdentityBase: "http://auth.url/",
},
region: "myRegion",
}
z, ok := os.Zones()
if !ok {
t.Fatalf("Zones() returned false")
}
zone, err := z.GetZone()
if err != nil {
t.Fatalf("GetZone() returned error: %s", err)
}
if zone.Region != "myRegion" {
t.Fatalf("GetZone() returned wrong region (%s)", zone.Region)
}
if zone.FailureDomain != "nova" {
t.Fatalf("GetZone() returned wrong failure domain (%s)", zone.FailureDomain)
}
}
var diskPathRegexp = regexp.MustCompile("/dev/disk/(?:by-id|by-path)/")
func TestVolumes(t *testing.T) {
cfg, ok := configFromEnv()
if !ok {
t.Skipf("No config found in environment")
}
os, err := newOpenStack(cfg)
if err != nil {
t.Fatalf("Failed to construct/authenticate OpenStack: %s", err)
}
tags := map[string]string{
"test": "value",
}
vol, _, _, err := os.CreateVolume("kubernetes-test-volume-"+rand.String(10), 1, "", "", &tags)
if err != nil {
t.Fatalf("Cannot create a new Cinder volume: %v", err)
}
t.Logf("Volume (%s) created\n", vol)
WaitForVolumeStatus(t, os, vol, volumeAvailableStatus)
id, err := os.InstanceID()
if err != nil {
t.Logf("Cannot find instance id: %v - perhaps you are running this test outside a VM launched by OpenStack", err)
} else {
diskId, err := os.AttachDisk(id, vol)
if err != nil {
t.Fatalf("Cannot AttachDisk Cinder volume %s: %v", vol, err)
}
t.Logf("Volume (%s) attached, disk ID: %s\n", vol, diskId)
WaitForVolumeStatus(t, os, vol, volumeInUseStatus)
devicePath := os.GetDevicePath(diskId)
if diskPathRegexp.FindString(devicePath) == "" {
t.Fatalf("GetDevicePath returned and unexpected path for Cinder volume %s, returned %s", vol, devicePath)
}
t.Logf("Volume (%s) found at path: %s\n", vol, devicePath)
err = os.DetachDisk(id, vol)
if err != nil {
t.Fatalf("Cannot DetachDisk Cinder volume %s: %v", vol, err)
}
t.Logf("Volume (%s) detached\n", vol)
WaitForVolumeStatus(t, os, vol, volumeAvailableStatus)
}
err = os.DeleteVolume(vol)
if err != nil {
t.Fatalf("Cannot delete Cinder volume %s: %v", vol, err)
}
t.Logf("Volume (%s) deleted\n", vol)
}
func TestInstanceIDFromProviderID(t *testing.T) {
testCases := []struct {
providerID string
instanceID string
fail bool
}{
{
providerID: ProviderName + "://" + "/" + "7b9cf879-7146-417c-abfd-cb4272f0c935",
instanceID: "7b9cf879-7146-417c-abfd-cb4272f0c935",
fail: false,
},
{
providerID: "openstack://7b9cf879-7146-417c-abfd-cb4272f0c935",
instanceID: "",
fail: true,
},
{
providerID: "7b9cf879-7146-417c-abfd-cb4272f0c935",
instanceID: "",
fail: true,
},
{
providerID: "other-provider:///7b9cf879-7146-417c-abfd-cb4272f0c935",
instanceID: "",
fail: true,
},
}
for _, test := range testCases {
instanceID, err := instanceIDFromProviderID(test.providerID)
if (err != nil) != test.fail {
t.Errorf("%s yielded `err != nil` as %t. expected %t", test.providerID, (err != nil), test.fail)
}
if test.fail {
continue
}
if instanceID != test.instanceID {
t.Errorf("%s yielded %s. expected %s", test.providerID, instanceID, test.instanceID)
}
}
}

View File

@ -0,0 +1,620 @@
/*
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 openstack
import (
"fmt"
"io/ioutil"
"path"
"path/filepath"
"strings"
"time"
"k8s.io/apimachinery/pkg/api/resource"
k8s_volume "k8s.io/kubernetes/pkg/volume"
"github.com/gophercloud/gophercloud"
volumeexpand "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
volumes_v1 "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes"
volumes_v2 "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
volumes_v3 "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach"
"github.com/prometheus/client_golang/prometheus"
"github.com/golang/glog"
)
type volumeService interface {
createVolume(opts VolumeCreateOpts) (string, string, error)
getVolume(volumeID string) (Volume, error)
deleteVolume(volumeName string) error
expandVolume(volumeID string, newSize int) error
}
// Volumes implementation for v1
type VolumesV1 struct {
blockstorage *gophercloud.ServiceClient
opts BlockStorageOpts
}
// Volumes implementation for v2
type VolumesV2 struct {
blockstorage *gophercloud.ServiceClient
opts BlockStorageOpts
}
// Volumes implementation for v3
type VolumesV3 struct {
blockstorage *gophercloud.ServiceClient
opts BlockStorageOpts
}
type Volume struct {
// ID of the instance, to which this volume is attached. "" if not attached
AttachedServerId string
// Device file path
AttachedDevice string
// Unique identifier for the volume.
ID string
// Human-readable display name for the volume.
Name string
// Current status of the volume.
Status string
// Volume size in GB
Size int
}
type VolumeCreateOpts struct {
Size int
Availability string
Name string
VolumeType string
Metadata map[string]string
}
const (
VolumeAvailableStatus = "available"
VolumeInUseStatus = "in-use"
VolumeDeletedStatus = "deleted"
VolumeErrorStatus = "error"
// On some environments, we need to query the metadata service in order
// to locate disks. We'll use the Newton version, which includes device
// metadata.
NewtonMetadataVersion = "2016-06-30"
)
func (volumes *VolumesV1) createVolume(opts VolumeCreateOpts) (string, string, error) {
startTime := time.Now()
create_opts := volumes_v1.CreateOpts{
Name: opts.Name,
Size: opts.Size,
VolumeType: opts.VolumeType,
AvailabilityZone: opts.Availability,
Metadata: opts.Metadata,
}
vol, err := volumes_v1.Create(volumes.blockstorage, create_opts).Extract()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("create_v1_volume", timeTaken, err)
if err != nil {
return "", "", err
}
return vol.ID, vol.AvailabilityZone, nil
}
func (volumes *VolumesV2) createVolume(opts VolumeCreateOpts) (string, string, error) {
startTime := time.Now()
create_opts := volumes_v2.CreateOpts{
Name: opts.Name,
Size: opts.Size,
VolumeType: opts.VolumeType,
AvailabilityZone: opts.Availability,
Metadata: opts.Metadata,
}
vol, err := volumes_v2.Create(volumes.blockstorage, create_opts).Extract()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("create_v2_volume", timeTaken, err)
if err != nil {
return "", "", err
}
return vol.ID, vol.AvailabilityZone, nil
}
func (volumes *VolumesV3) createVolume(opts VolumeCreateOpts) (string, string, error) {
startTime := time.Now()
create_opts := volumes_v3.CreateOpts{
Name: opts.Name,
Size: opts.Size,
VolumeType: opts.VolumeType,
AvailabilityZone: opts.Availability,
Metadata: opts.Metadata,
}
vol, err := volumes_v3.Create(volumes.blockstorage, create_opts).Extract()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("create_v3_volume", timeTaken, err)
if err != nil {
return "", "", err
}
return vol.ID, vol.AvailabilityZone, nil
}
func (volumes *VolumesV1) getVolume(volumeID string) (Volume, error) {
startTime := time.Now()
volumeV1, err := volumes_v1.Get(volumes.blockstorage, volumeID).Extract()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("get_v1_volume", timeTaken, err)
if err != nil {
return Volume{}, fmt.Errorf("error occurred getting volume by ID: %s, err: %v", volumeID, err)
}
volume := Volume{
ID: volumeV1.ID,
Name: volumeV1.Name,
Status: volumeV1.Status,
Size: volumeV1.Size,
}
if len(volumeV1.Attachments) > 0 && volumeV1.Attachments[0]["server_id"] != nil {
volume.AttachedServerId = volumeV1.Attachments[0]["server_id"].(string)
volume.AttachedDevice = volumeV1.Attachments[0]["device"].(string)
}
return volume, nil
}
func (volumes *VolumesV2) getVolume(volumeID string) (Volume, error) {
startTime := time.Now()
volumeV2, err := volumes_v2.Get(volumes.blockstorage, volumeID).Extract()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("get_v2_volume", timeTaken, err)
if err != nil {
return Volume{}, fmt.Errorf("error occurred getting volume by ID: %s, err: %v", volumeID, err)
}
volume := Volume{
ID: volumeV2.ID,
Name: volumeV2.Name,
Status: volumeV2.Status,
Size: volumeV2.Size,
}
if len(volumeV2.Attachments) > 0 {
volume.AttachedServerId = volumeV2.Attachments[0].ServerID
volume.AttachedDevice = volumeV2.Attachments[0].Device
}
return volume, nil
}
func (volumes *VolumesV3) getVolume(volumeID string) (Volume, error) {
startTime := time.Now()
volumeV3, err := volumes_v3.Get(volumes.blockstorage, volumeID).Extract()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("get_v3_volume", timeTaken, err)
if err != nil {
return Volume{}, fmt.Errorf("error occurred getting volume by ID: %s, err: %v", volumeID, err)
}
volume := Volume{
ID: volumeV3.ID,
Name: volumeV3.Name,
Status: volumeV3.Status,
}
if len(volumeV3.Attachments) > 0 {
volume.AttachedServerId = volumeV3.Attachments[0].ServerID
volume.AttachedDevice = volumeV3.Attachments[0].Device
}
return volume, nil
}
func (volumes *VolumesV1) deleteVolume(volumeID string) error {
startTime := time.Now()
err := volumes_v1.Delete(volumes.blockstorage, volumeID).ExtractErr()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("delete_v1_volume", timeTaken, err)
return err
}
func (volumes *VolumesV2) deleteVolume(volumeID string) error {
startTime := time.Now()
err := volumes_v2.Delete(volumes.blockstorage, volumeID).ExtractErr()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("delete_v2_volume", timeTaken, err)
return err
}
func (volumes *VolumesV3) deleteVolume(volumeID string) error {
startTime := time.Now()
err := volumes_v3.Delete(volumes.blockstorage, volumeID).ExtractErr()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("delete_v3_volume", timeTaken, err)
return err
}
func (volumes *VolumesV1) expandVolume(volumeID string, newSize int) error {
startTime := time.Now()
create_opts := volumeexpand.ExtendSizeOpts{
NewSize: newSize,
}
err := volumeexpand.ExtendSize(volumes.blockstorage, volumeID, create_opts).ExtractErr()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("expand_volume", timeTaken, err)
return err
}
func (volumes *VolumesV2) expandVolume(volumeID string, newSize int) error {
startTime := time.Now()
create_opts := volumeexpand.ExtendSizeOpts{
NewSize: newSize,
}
err := volumeexpand.ExtendSize(volumes.blockstorage, volumeID, create_opts).ExtractErr()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("expand_volume", timeTaken, err)
return err
}
func (volumes *VolumesV3) expandVolume(volumeID string, newSize int) error {
startTime := time.Now()
create_opts := volumeexpand.ExtendSizeOpts{
NewSize: newSize,
}
err := volumeexpand.ExtendSize(volumes.blockstorage, volumeID, create_opts).ExtractErr()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("expand_volume", timeTaken, err)
return err
}
func (os *OpenStack) OperationPending(diskName string) (bool, string, error) {
volume, err := os.getVolume(diskName)
if err != nil {
return false, "", err
}
volumeStatus := volume.Status
if volumeStatus == VolumeErrorStatus {
return false, volumeStatus, nil
}
if volumeStatus == VolumeAvailableStatus || volumeStatus == VolumeInUseStatus || volumeStatus == VolumeDeletedStatus {
return false, volume.Status, nil
}
return true, volumeStatus, nil
}
// AttachDisk attaches given cinder volume to the compute running kubelet
func (os *OpenStack) AttachDisk(instanceID, volumeID string) (string, error) {
volume, err := os.getVolume(volumeID)
if err != nil {
return "", err
}
cClient, err := os.NewComputeV2()
if err != nil {
return "", err
}
if volume.AttachedServerId != "" {
if instanceID == volume.AttachedServerId {
glog.V(4).Infof("Disk %s is already attached to instance %s", volumeID, instanceID)
return volume.ID, nil
}
return "", fmt.Errorf("disk %s is attached to a different instance (%s)", volumeID, volume.AttachedServerId)
}
startTime := time.Now()
// add read only flag here if possible spothanis
_, err = volumeattach.Create(cClient, instanceID, &volumeattach.CreateOpts{
VolumeID: volume.ID,
}).Extract()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("attach_disk", timeTaken, err)
if err != nil {
return "", fmt.Errorf("failed to attach %s volume to %s compute: %v", volumeID, instanceID, err)
}
glog.V(2).Infof("Successfully attached %s volume to %s compute", volumeID, instanceID)
return volume.ID, nil
}
// DetachDisk detaches given cinder volume from the compute running kubelet
func (os *OpenStack) DetachDisk(instanceID, volumeID string) error {
volume, err := os.getVolume(volumeID)
if err != nil {
return err
}
if volume.Status == VolumeAvailableStatus {
// "available" is fine since that means the volume is detached from instance already.
glog.V(2).Infof("volume: %s has been detached from compute: %s ", volume.ID, instanceID)
return nil
}
if volume.Status != VolumeInUseStatus {
return fmt.Errorf("can not detach volume %s, its status is %s", volume.Name, volume.Status)
}
cClient, err := os.NewComputeV2()
if err != nil {
return err
}
if volume.AttachedServerId != instanceID {
return fmt.Errorf("disk: %s has no attachments or is not attached to compute: %s", volume.Name, instanceID)
} else {
startTime := time.Now()
// This is a blocking call and effects kubelet's performance directly.
// We should consider kicking it out into a separate routine, if it is bad.
err = volumeattach.Delete(cClient, instanceID, volume.ID).ExtractErr()
timeTaken := time.Since(startTime).Seconds()
recordOpenstackOperationMetric("detach_disk", timeTaken, err)
if err != nil {
return fmt.Errorf("failed to delete volume %s from compute %s attached %v", volume.ID, instanceID, err)
}
glog.V(2).Infof("Successfully detached volume: %s from compute: %s", volume.ID, instanceID)
}
return nil
}
// ExpandVolume expands the size of specific cinder volume (in GiB)
func (os *OpenStack) ExpandVolume(volumeID string, oldSize resource.Quantity, newSize resource.Quantity) (resource.Quantity, error) {
volume, err := os.getVolume(volumeID)
if err != nil {
return oldSize, err
}
if volume.Status != VolumeAvailableStatus {
// cinder volume can not be expanded if its status is not available
return oldSize, fmt.Errorf("volume status is not available")
}
volSizeBytes := newSize.Value()
// Cinder works with gigabytes, convert to GiB with rounding up
volSizeGB := int(k8s_volume.RoundUpSize(volSizeBytes, 1024*1024*1024))
newSizeQuant := resource.MustParse(fmt.Sprintf("%dGi", volSizeGB))
// if volume size equals to or greater than the newSize, return nil
if volume.Size >= volSizeGB {
return newSizeQuant, nil
}
volumes, err := os.volumeService("")
if err != nil {
return oldSize, err
}
err = volumes.expandVolume(volumeID, volSizeGB)
if err != nil {
return oldSize, err
}
return newSizeQuant, nil
}
// getVolume retrieves Volume by its ID.
func (os *OpenStack) getVolume(volumeID string) (Volume, error) {
volumes, err := os.volumeService("")
if err != nil {
return Volume{}, fmt.Errorf("unable to initialize cinder client for region: %s, err: %v", os.region, err)
}
return volumes.getVolume(volumeID)
}
// CreateVolume creates a volume of given size (in GiB)
func (os *OpenStack) CreateVolume(name string, size int, vtype, availability string, tags *map[string]string) (string, string, bool, error) {
volumes, err := os.volumeService("")
if err != nil {
return "", "", os.bsOpts.IgnoreVolumeAZ, fmt.Errorf("unable to initialize cinder client for region: %s, err: %v", os.region, err)
}
opts := VolumeCreateOpts{
Name: name,
Size: size,
VolumeType: vtype,
Availability: availability,
}
if tags != nil {
opts.Metadata = *tags
}
volumeID, volumeAZ, err := volumes.createVolume(opts)
if err != nil {
return "", "", os.bsOpts.IgnoreVolumeAZ, fmt.Errorf("failed to create a %d GB volume: %v", size, err)
}
glog.Infof("Created volume %v in Availability Zone: %v Ignore volume AZ: %v", volumeID, volumeAZ, os.bsOpts.IgnoreVolumeAZ)
return volumeID, volumeAZ, os.bsOpts.IgnoreVolumeAZ, nil
}
// GetDevicePath returns the path of an attached block storage volume, specified by its id.
func (os *OpenStack) GetDevicePathBySerialId(volumeID string) string {
// Build a list of candidate device paths.
// Certain Nova drivers will set the disk serial ID, including the Cinder volume id.
candidateDeviceNodes := []string{
// KVM
fmt.Sprintf("virtio-%s", volumeID[:20]),
// KVM virtio-scsi
fmt.Sprintf("scsi-0QEMU_QEMU_HARDDISK_%s", volumeID[:20]),
// ESXi
fmt.Sprintf("wwn-0x%s", strings.Replace(volumeID, "-", "", -1)),
}
files, _ := ioutil.ReadDir("/dev/disk/by-id/")
for _, f := range files {
for _, c := range candidateDeviceNodes {
if c == f.Name() {
glog.V(4).Infof("Found disk attached as %q; full devicepath: %s\n", f.Name(), path.Join("/dev/disk/by-id/", f.Name()))
return path.Join("/dev/disk/by-id/", f.Name())
}
}
}
glog.V(4).Infof("Failed to find device for the volumeID: %q by serial ID", volumeID)
return ""
}
func (os *OpenStack) GetDevicePathFromInstanceMetadata(volumeID string) string {
// Nova Hyper-V hosts cannot override disk SCSI IDs. In order to locate
// volumes, we're querying the metadata service. Note that the Hyper-V
// driver will include device metadata for untagged volumes as well.
//
// We're avoiding using cached metadata (or the configdrive),
// relying on the metadata service.
instanceMetadata, err := getMetadataFromMetadataService(
NewtonMetadataVersion)
if err != nil {
glog.V(4).Infof(
"Could not retrieve instance metadata. Error: %v", err)
return ""
}
for _, device := range instanceMetadata.Devices {
if device.Type == "disk" && device.Serial == volumeID {
glog.V(4).Infof(
"Found disk metadata for volumeID %q. Bus: %q, Address: %q",
volumeID, device.Bus, device.Address)
diskPattern := fmt.Sprintf(
"/dev/disk/by-path/*-%s-%s",
device.Bus, device.Address)
diskPaths, err := filepath.Glob(diskPattern)
if err != nil {
glog.Errorf(
"could not retrieve disk path for volumeID: %q. Error filepath.Glob(%q): %v",
volumeID, diskPattern, err)
return ""
}
if len(diskPaths) == 1 {
return diskPaths[0]
}
glog.Errorf(
"expecting to find one disk path for volumeID %q, found %d: %v",
volumeID, len(diskPaths), diskPaths)
return ""
}
}
glog.V(4).Infof(
"Could not retrieve device metadata for volumeID: %q", volumeID)
return ""
}
// GetDevicePath returns the path of an attached block storage volume, specified by its id.
func (os *OpenStack) GetDevicePath(volumeID string) string {
devicePath := os.GetDevicePathBySerialId(volumeID)
if devicePath == "" {
devicePath = os.GetDevicePathFromInstanceMetadata(volumeID)
}
if devicePath == "" {
glog.Warningf("Failed to find device for the volumeID: %q", volumeID)
}
return devicePath
}
func (os *OpenStack) DeleteVolume(volumeID string) error {
used, err := os.diskIsUsed(volumeID)
if err != nil {
return err
}
if used {
msg := fmt.Sprintf("Cannot delete the volume %q, it's still attached to a node", volumeID)
return k8s_volume.NewDeletedVolumeInUseError(msg)
}
volumes, err := os.volumeService("")
if err != nil {
return fmt.Errorf("unable to initialize cinder client for region: %s, err: %v", os.region, err)
}
err = volumes.deleteVolume(volumeID)
return err
}
// GetAttachmentDiskPath gets device path of attached volume to the compute running kubelet, as known by cinder
func (os *OpenStack) GetAttachmentDiskPath(instanceID, volumeID string) (string, error) {
// See issue #33128 - Cinder does not always tell you the right device path, as such
// we must only use this value as a last resort.
volume, err := os.getVolume(volumeID)
if err != nil {
return "", err
}
if volume.Status != VolumeInUseStatus {
return "", fmt.Errorf("can not get device path of volume %s, its status is %s ", volume.Name, volume.Status)
}
if volume.AttachedServerId != "" {
if instanceID == volume.AttachedServerId {
// Attachment[0]["device"] points to the device path
// see http://developer.openstack.org/api-ref-blockstorage-v1.html
return volume.AttachedDevice, nil
} else {
return "", fmt.Errorf("disk %q is attached to a different compute: %q, should be detached before proceeding", volumeID, volume.AttachedServerId)
}
}
return "", fmt.Errorf("volume %s has no ServerId", volumeID)
}
// DiskIsAttached queries if a volume is attached to a compute instance
func (os *OpenStack) DiskIsAttached(instanceID, volumeID string) (bool, error) {
volume, err := os.getVolume(volumeID)
if err != nil {
return false, err
}
return instanceID == volume.AttachedServerId, nil
}
// DisksAreAttached queries if a list of volumes are attached to a compute instance
func (os *OpenStack) DisksAreAttached(instanceID string, volumeIDs []string) (map[string]bool, error) {
attached := make(map[string]bool)
for _, volumeID := range volumeIDs {
isAttached, _ := os.DiskIsAttached(instanceID, volumeID)
attached[volumeID] = isAttached
}
return attached, nil
}
// diskIsUsed returns true a disk is attached to any node.
func (os *OpenStack) diskIsUsed(volumeID string) (bool, error) {
volume, err := os.getVolume(volumeID)
if err != nil {
return false, err
}
return volume.AttachedServerId != "", nil
}
// ShouldTrustDevicePath queries if we should trust the cinder provide deviceName, See issue #33128
func (os *OpenStack) ShouldTrustDevicePath() bool {
return os.bsOpts.TrustDevicePath
}
// recordOpenstackOperationMetric records openstack operation metrics
func recordOpenstackOperationMetric(operation string, timeTaken float64, err error) {
if err != nil {
OpenstackApiRequestErrors.With(prometheus.Labels{"request": operation}).Inc()
} else {
OpenstackOperationsLatency.With(prometheus.Labels{"request": operation}).Observe(timeTaken)
}
}