diff --git a/pkg/cloudprovider/openstack/openstack.go b/pkg/cloudprovider/openstack/openstack.go index d4b8b8c2c7d..b1615bee39a 100644 --- a/pkg/cloudprovider/openstack/openstack.go +++ b/pkg/cloudprovider/openstack/openstack.go @@ -17,15 +17,25 @@ limitations under the License. package openstack import ( + "errors" "fmt" "io" + "net" + "net/url" + "regexp" "code.google.com/p/gcfg" "github.com/rackspace/gophercloud" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) +var ErrServerNotFound = errors.New("Server not found") +var ErrMultipleServersFound = errors.New("Multiple servers matched query") +var ErrFlavorNotFound = errors.New("Flavor not found") + // OpenStack is an implementation of cloud provider Interface for OpenStack. type OpenStack struct { provider string @@ -91,11 +101,114 @@ func newOpenStack(cfg Config) (*OpenStack, error) { return &os, err } -func (os *OpenStack) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) { - return nil, false +type Instances struct { + servers gophercloud.CloudServersProvider + flavor_to_resource map[string]*api.NodeResources // keyed by flavor id } +// Instances returns an implementation of Instances for OpenStack. func (os *OpenStack) Instances() (cloudprovider.Instances, bool) { + servers, err := gophercloud.ServersApi(os.access, gophercloud.ApiCriteria{ + Type: "compute", + UrlChoice: gophercloud.PublicURL, + Region: os.region, + }) + + if err != nil { + return nil, false + } + + flavors, err := servers.ListFlavors() + if err != nil { + return nil, false + } + flavor_to_resource := make(map[string]*api.NodeResources, len(flavors)) + for _, flavor := range flavors { + rsrc := api.NodeResources{ + Capacity: api.ResourceList{ + "cpu": util.NewIntOrStringFromInt(flavor.VCpus), + "memory": util.NewIntOrStringFromString(fmt.Sprintf("%dMiB", flavor.Ram)), + "openstack.org/disk": util.NewIntOrStringFromString(fmt.Sprintf("%dGB", flavor.Disk)), + "openstack.org/rxTxFactor": util.NewIntOrStringFromInt(int(flavor.RxTxFactor * 1000)), + "openstack.org/swap": util.NewIntOrStringFromString(fmt.Sprintf("%dMiB", flavor.Swap)), + }, + } + flavor_to_resource[flavor.Id] = &rsrc + } + + return &Instances{servers, flavor_to_resource}, true +} + +func (i *Instances) List(name_filter string) ([]string, error) { + filter := url.Values{} + filter.Set("name", name_filter) + filter.Set("status", "ACTIVE") + + servers, err := i.servers.ListServersByFilter(filter) + if err != nil { + return nil, err + } + + ret := make([]string, len(servers)) + for idx, srv := range servers { + ret[idx] = srv.Name + } + return ret, nil +} + +func getServerByName(api gophercloud.CloudServersProvider, name string) (*gophercloud.Server, error) { + filter := url.Values{} + filter.Set("name", fmt.Sprintf("^%s$", regexp.QuoteMeta(name))) + filter.Set("status", "ACTIVE") + + servers, err := api.ListServersByFilter(filter) + if err != nil { + return nil, err + } + + if len(servers) == 0 { + return nil, ErrServerNotFound + } else if len(servers) > 1 { + return nil, ErrMultipleServersFound + } + + return &servers[0], nil +} + +func (i *Instances) IPAddress(name string) (net.IP, error) { + srv, err := getServerByName(i.servers, name) + if err != nil { + return nil, err + } + + var s string + if len(srv.Addresses.Private) > 0 { + s = srv.Addresses.Private[0].Addr + } else if len(srv.Addresses.Public) > 0 { + s = srv.Addresses.Public[0].Addr + } else if srv.AccessIPv4 != "" { + s = srv.AccessIPv4 + } else { + s = srv.AccessIPv6 + } + return net.ParseIP(s), nil +} + +func (i *Instances) GetNodeResources(name string) (*api.NodeResources, error) { + srv, err := getServerByName(i.servers, name) + if err != nil { + return nil, err + } + + rsrc, ok := i.flavor_to_resource[srv.Flavor.Id] + if !ok { + return nil, ErrFlavorNotFound + } + + return rsrc, nil +} + +func (os *OpenStack) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) { return nil, false } diff --git a/pkg/cloudprovider/openstack/openstack_test.go b/pkg/cloudprovider/openstack/openstack_test.go index 079391cbb4c..eada1794b7b 100644 --- a/pkg/cloudprovider/openstack/openstack_test.go +++ b/pkg/cloudprovider/openstack/openstack_test.go @@ -17,11 +17,12 @@ limitations under the License. package openstack import ( + "os" "strings" "testing" ) -func TestNewOpenStack(t *testing.T) { +func TestReadConfig(t *testing.T) { _, err := readConfig(nil) if err == nil { t.Errorf("Should fail when no config is provided: %s", err) @@ -54,3 +55,81 @@ func TestToAuthOptions(t *testing.T) { t.Errorf("Username %s != %s", ao.Username, cfg.Global.Username) } } + +// This allows testing against an existing OpenStack install, using the +// standard OS_* OpenStack client environment variables. +func configFromEnv() (cfg Config, ok bool) { + cfg.Global.AuthUrl = os.Getenv("OS_AUTH_URL") + // gophercloud wants "provider" to point specifically at tokens URL + if !strings.HasSuffix(cfg.Global.AuthUrl, "/tokens") { + cfg.Global.AuthUrl += "/tokens" + } + + 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.ApiKey = os.Getenv("OS_API_KEY") + cfg.Global.Region = os.Getenv("OS_REGION_NAME") + + ok = (cfg.Global.AuthUrl != "" && + cfg.Global.Username != "" && + (cfg.Global.Password != "" || cfg.Global.ApiKey != "") && + (cfg.Global.TenantId != "" || cfg.Global.TenantName != "")) + + 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 TestInstances(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) + } + + i, ok := os.Instances() + if !ok { + t.Fatalf("Instances() returned false") + } + + srvs, err := i.List(".") + if err != nil { + t.Fatalf("Instances.List() failed: %s", err) + } + if len(srvs) == 0 { + t.Fatalf("Instances.List() returned zero servers") + } + t.Logf("Found servers (%d): %s\n", len(srvs), srvs) + + ip, err := i.IPAddress(srvs[0]) + if err != nil { + t.Fatalf("Instances.IPAddress(%s) failed: %s", srvs[0], err) + } + t.Logf("Found IPAddress(%s) = %s\n", srvs[0], ip) + + rsrcs, err := i.GetNodeResources(srvs[0]) + if err != nil { + t.Fatalf("Instances.GetNodeResources(%s) failed: %s", srvs[0], err) + } + t.Logf("Found GetNodeResources(%s) = %s\n", srvs[0], rsrcs) +}