From 320fd528e277a2a27fb6870516696055f01bc07d Mon Sep 17 00:00:00 2001 From: Federico Simoncelli Date: Thu, 28 Aug 2014 17:30:49 +0000 Subject: [PATCH] Add support for oVirt cloud provider This patch adds the initial support for the oVirt cloud provider. Signed-off-by: Federico Simoncelli --- cluster/ovirt/ovirt-cloud.conf | 9 ++ cmd/apiserver/plugins.go | 1 + pkg/cloudprovider/ovirt/ovirt.go | 156 ++++++++++++++++++++++++++ pkg/cloudprovider/ovirt/ovirt_test.go | 124 ++++++++++++++++++++ 4 files changed, 290 insertions(+) create mode 100644 cluster/ovirt/ovirt-cloud.conf create mode 100644 pkg/cloudprovider/ovirt/ovirt.go create mode 100644 pkg/cloudprovider/ovirt/ovirt_test.go diff --git a/cluster/ovirt/ovirt-cloud.conf b/cluster/ovirt/ovirt-cloud.conf new file mode 100644 index 00000000000..68dc19c7e05 --- /dev/null +++ b/cluster/ovirt/ovirt-cloud.conf @@ -0,0 +1,9 @@ +# ovirt-cloud.conf +[connection] +uri = https://localhost:8443/ovirt-engine/api +username = admin@internal +password = admin + +[filters] +# Search queries used to find minions +vms = tag=kubernetes diff --git a/cmd/apiserver/plugins.go b/cmd/apiserver/plugins.go index 9463332c590..266b57d108a 100644 --- a/cmd/apiserver/plugins.go +++ b/cmd/apiserver/plugins.go @@ -22,4 +22,5 @@ package main import ( _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant" + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt" ) diff --git a/pkg/cloudprovider/ovirt/ovirt.go b/pkg/cloudprovider/ovirt/ovirt.go new file mode 100644 index 00000000000..9255d4bf7aa --- /dev/null +++ b/pkg/cloudprovider/ovirt/ovirt.go @@ -0,0 +1,156 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 ovirt_cloud + +import ( + "encoding/xml" + "io" + "io/ioutil" + "fmt" + "net" + "net/http" + "net/url" + "path" + "strings" + + "code.google.com/p/gcfg" + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" +) + +type OVirtCloud struct { + VmsRequest *url.URL + HostsRequest *url.URL +} + +type OVirtApiConfig struct { + Connection struct { + ApiEntry string `gcfg:"uri"` + Username string `gcfg:"username"` + Password string `gcfg:"password"` + } + Filters struct { + VmsQuery string `gcfg:"vms"` + } +} + +type XmlVmInfo struct { + Hostname string `xml:"guest_info>fqdn"` + State string `xml:"status>state"` +} + +type XmlVmsList struct { + XMLName xml.Name `xml:"vms"` + Vm []XmlVmInfo `xml:"vm"` +} + +func init() { + cloudprovider.RegisterCloudProvider("ovirt", + func(config io.Reader) (cloudprovider.Interface, error) { + return newOVirtCloud(config) + }) +} + +func newOVirtCloud(config io.Reader) (*OVirtCloud, error) { + if config == nil { + return nil, fmt.Errorf("missing configuration file for ovirt cloud provider") + } + + oVirtConfig := OVirtApiConfig{} + + /* defaults */ + oVirtConfig.Connection.Username = "admin@internal" + + if err := gcfg.ReadInto(&oVirtConfig, config); err != nil { + return nil, err + } + + if oVirtConfig.Connection.ApiEntry == "" { + return nil, fmt.Errorf("missing ovirt uri in cloud provider configuration") + } + + request, err := url.Parse(oVirtConfig.Connection.ApiEntry) + if err != nil { + return nil, err + } + + request.Path = path.Join(request.Path, "vms") + request.User = url.UserPassword(oVirtConfig.Connection.Username, oVirtConfig.Connection.Password) + request.RawQuery = url.Values{"search": {oVirtConfig.Filters.VmsQuery}}.Encode() + + return &OVirtCloud{VmsRequest: request}, nil +} + +// TCPLoadBalancer returns an implementation of TCPLoadBalancer for oVirt cloud +func (v *OVirtCloud) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) { + return nil, false +} + +// Instances returns an implementation of Instances for oVirt cloud +func (v *OVirtCloud) Instances() (cloudprovider.Instances, bool) { + return v, true +} + +// Zones returns an implementation of Zones for oVirt cloud +func (v *OVirtCloud) Zones() (cloudprovider.Zones, bool) { + return nil, false +} + +// IPAddress returns the address of a particular machine instance +func (v *OVirtCloud) IPAddress(instance string) (net.IP, error) { + // since the instance now is the IP in the ovirt env, this is trivial no-op + return net.ParseIP(instance), nil +} + +func getInstancesFromXml(body io.Reader) ([]string, error) { + if body == nil { + return nil, fmt.Errorf("ovirt rest-api response body is missing") + } + + content, err := ioutil.ReadAll(body) + if err != nil { + return nil, err + } + + vmlist := XmlVmsList{} + + if err := xml.Unmarshal(content, &vmlist); err != nil { + return nil, err + } + + var instances []string + + for _, vm := range vmlist.Vm { + // Always return only vms that are up and running + if vm.Hostname != "" && strings.ToLower(vm.State) == "up" { + instances = append(instances, vm.Hostname) + } + } + + return instances, nil +} + +// List enumerates the set of minions instances known by the cloud provider +func (v *OVirtCloud) List(filter string) ([]string, error) { + response, err := http.Get(v.VmsRequest.String()) + if err != nil { + return nil, err + } + + defer response.Body.Close() + + return getInstancesFromXml(response.Body) +} diff --git a/pkg/cloudprovider/ovirt/ovirt_test.go b/pkg/cloudprovider/ovirt/ovirt_test.go new file mode 100644 index 00000000000..ceb608876dd --- /dev/null +++ b/pkg/cloudprovider/ovirt/ovirt_test.go @@ -0,0 +1,124 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 ovirt_cloud + +import ( + "io" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" +) + +func TestOVirtCloudConfiguration(t *testing.T) { + config1 := (io.Reader)(nil) + + _, err1 := cloudprovider.GetCloudProvider("ovirt", config1) + if err1 == nil { + t.Fatalf("An error is expected when the configuration is missing") + } + + config2 := strings.NewReader("") + + _, err2 := cloudprovider.GetCloudProvider("ovirt", config2) + if err2 == nil { + t.Fatalf("An error is expected when the configuration is empty") + } + + config3 := strings.NewReader(` +[connection] + `) + + _, err3 := cloudprovider.GetCloudProvider("ovirt", config3) + if err3 == nil { + t.Fatalf("An error is expected when the uri is missing") + } + + config4 := strings.NewReader(` +[connection] +uri = https://localhost:8443/ovirt-engine/api +`) + + _, err4 := cloudprovider.GetCloudProvider("ovirt", config4) + if err4 != nil { + t.Fatalf("Unexpected error creating the provider: %s", err4) + } +} + +func TestOVirtCloudXmlParsing(t *testing.T) { + body1 := (io.Reader)(nil) + + _, err1 := getInstancesFromXml(body1) + if err1 == nil { + t.Fatalf("An error is expected when body is missing") + } + + body2 := strings.NewReader("") + + _, err2 := getInstancesFromXml(body2) + if err2 == nil { + t.Fatalf("An error is expected when body is empty") + } + + body3 := strings.NewReader(` + + + +`) + + instances3, err3 := getInstancesFromXml(body3) + if err3 != nil { + t.Fatalf("Unexpected error listing instances: %s", err3) + } + if len(instances3) > 0 { + t.Fatalf("Unexpected number of instance(s): %i", len(instances3)) + } + + body4 := strings.NewReader(` + + + Up + host1 + + + + + + Up + + + Down + host2 + + + Up + host3 + + +`) + + instances4, err4 := getInstancesFromXml(body4) + if err4 != nil { + t.Fatalf("Unexpected error listing instances: %s", err4) + } + if len(instances4) != 2 { + t.Fatalf("Unexpected number of instance(s): %i", len(instances4)) + } + if instances4[0] != "host1" || instances4[1] != "host3" { + t.Fatalf("Unexpected instance(s): %s", instances4) + } +}