kops/upup/pkg/fi/cloudup/openstacktasks/instance.go

429 lines
11 KiB
Go

/*
Copyright 2019 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 openstacktasks
import (
"fmt"
"strconv"
"strings"
l3floatingip "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"k8s.io/klog/v2"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
)
// +kops:fitask
type Instance struct {
ID *string
Name *string
GroupName *string
Port *Port
Region *string
Flavor *string
Image *string
SSHKey *string
ServerGroup *ServerGroup
Role *string
UserData fi.Resource
Metadata map[string]string
AvailabilityZone *string
SecurityGroups []string
FloatingIP *FloatingIP
Lifecycle *fi.Lifecycle
ForAPIServer bool
}
var _ fi.Task = &Instance{}
var _ fi.HasAddress = &Instance{}
var _ fi.HasDependencies = &Instance{}
// GetDependencies returns the dependencies of the Instance task
func (e *Instance) GetDependencies(tasks map[string]fi.Task) []fi.Task {
var deps []fi.Task
for _, task := range tasks {
if _, ok := task.(*ServerGroup); ok {
deps = append(deps, task)
}
if _, ok := task.(*Port); ok {
deps = append(deps, task)
}
if _, ok := task.(*FloatingIP); ok {
deps = append(deps, task)
}
}
if e.UserData != nil {
deps = append(deps, fi.FindDependencies(tasks, e.UserData)...)
}
return deps
}
var _ fi.CompareWithID = &Instance{}
func (e *Instance) WaitForStatusActive(t *openstack.OpenstackAPITarget) error {
return servers.WaitForStatus(t.Cloud.ComputeClient(), *e.ID, "ACTIVE", 120)
}
func (e *Instance) CompareWithID() *string {
return e.ID
}
func (e *Instance) IsForAPIServer() bool {
return e.ForAPIServer
}
func (e *Instance) FindIPAddress(context *fi.Context) (*string, error) {
cloud := context.Cloud.(openstack.OpenstackCloud)
if e.Port == nil {
return nil, nil
}
ports, err := cloud.GetPort(fi.StringValue(e.Port.ID))
if err != nil {
return nil, err
}
for _, port := range ports.FixedIPs {
return fi.String(port.IPAddress), nil
}
return nil, nil
}
func (e *Instance) Find(c *fi.Context) (*Instance, error) {
if e == nil || e.Name == nil {
return nil, nil
}
cloud := c.Cloud.(openstack.OpenstackCloud)
computeClient := cloud.ComputeClient()
serverPage, err := servers.List(computeClient, servers.ListOpts{
Name: fmt.Sprintf("^%s", fi.StringValue(e.GroupName)),
}).AllPages()
if err != nil {
return nil, fmt.Errorf("error listing servers: %v", err)
}
serverList, err := servers.ExtractServers(serverPage)
if err != nil {
return nil, fmt.Errorf("error extracting server page: %v", err)
}
var filteredList []servers.Server
for _, server := range serverList {
val, ok := server.Metadata["k8s"]
if !ok || val != fi.StringValue(e.ServerGroup.ClusterName) {
continue
}
metadataName := ""
val, ok = server.Metadata[openstack.TagKopsName]
if ok {
metadataName = val
}
// name or metadata tag should match to instance name
// this is needed for backwards compatibility
if server.Name == fi.StringValue(e.Name) || metadataName == fi.StringValue(e.Name) {
filteredList = append(filteredList, server)
}
}
if filteredList == nil {
return nil, nil
}
if len(filteredList) > 1 {
return nil, fmt.Errorf("Multiple servers found with name %s", fi.StringValue(e.Name))
}
server := filteredList[0]
actual := &Instance{
ID: fi.String(server.ID),
Name: e.Name,
SSHKey: fi.String(server.KeyName),
Lifecycle: e.Lifecycle,
Metadata: server.Metadata,
Role: fi.String(server.Metadata["KopsRole"]),
AvailabilityZone: e.AvailabilityZone,
GroupName: e.GroupName,
}
ports, err := cloud.ListPorts(ports.ListOpts{
DeviceID: server.ID,
})
if err != nil {
return nil, fmt.Errorf("failed to fetch port for instance %v: %v", server.ID, err)
}
if len(ports) == 1 {
port := ports[0]
porttask, err := newPortTaskFromCloud(cloud, e.Lifecycle, &port, nil)
if err != nil {
return nil, fmt.Errorf("failed to fetch port for instance %v: %v", server.ID, err)
}
actual.Port = porttask
} else if len(ports) > 1 {
return nil, fmt.Errorf("found more than one port for instance %v", server.ID)
}
if e.FloatingIP != nil && e.Port != nil {
fips, err := cloud.ListL3FloatingIPs(l3floatingip.ListOpts{
PortID: fi.StringValue(e.Port.ID),
})
if err != nil {
return nil, fmt.Errorf("failed to fetch floating ips for instance %v: %v", server.ID, err)
}
if len(fips) == 1 {
fip := fips[0]
fipTask := &FloatingIP{
ID: fi.String(fip.ID),
Name: fi.String(fip.Description),
}
actual.FloatingIP = fipTask
} else if len(fips) > 1 {
return nil, fmt.Errorf("found more than one floating ip for instance %v", server.ID)
}
}
// Avoid flapping
e.ID = actual.ID
actual.ForAPIServer = e.ForAPIServer
// Immutable fields
actual.Flavor = e.Flavor
actual.Image = e.Image
actual.UserData = e.UserData
actual.Region = e.Region
actual.SSHKey = e.SSHKey
actual.ServerGroup = e.ServerGroup
return actual, nil
}
func (e *Instance) Run(c *fi.Context) error {
return fi.DefaultDeltaRunMethod(e, c)
}
func (_ *Instance) CheckChanges(a, e, changes *Instance) error {
if a == nil {
if e.Name == nil {
return fi.RequiredField("Name")
}
} else {
if changes.ID != nil {
return fi.CannotChangeField("ID")
}
if changes.Name != nil {
return fi.CannotChangeField("Name")
}
}
return nil
}
func (_ *Instance) ShouldCreate(a, e, changes *Instance) (bool, error) {
if a == nil {
return true, nil
}
if changes.Port != nil {
return true, nil
}
if changes.FloatingIP != nil {
return true, nil
}
return false, nil
}
// generateInstanceName generates name for the instance
// the instance format is [GroupName]-[6 character hash]
func generateInstanceName(e *Instance) (string, error) {
secret, err := fi.CreateSecret()
if err != nil {
return "", err
}
hash, err := secret.AsString()
if err != nil {
return "", err
}
return strings.ToLower(fmt.Sprintf("%s-%s", fi.StringValue(e.GroupName), hash[0:6])), nil
}
func (_ *Instance) RenderOpenstack(t *openstack.OpenstackAPITarget, a, e, changes *Instance) error {
cloud := t.Cloud.(openstack.OpenstackCloud)
if a == nil {
serverName, err := generateInstanceName(e)
if err != nil {
return err
}
klog.V(2).Infof("Creating Instance with name: %q", serverName)
imageName := fi.StringValue(e.Image)
image, err := cloud.GetImage(imageName)
if err != nil {
return fmt.Errorf("failed to find image %v: %v", imageName, err)
}
flavorName := fi.StringValue(e.Flavor)
flavor, err := cloud.GetFlavor(flavorName)
if err != nil {
return fmt.Errorf("failed to find flavor %v: %v", flavorName, err)
}
opt := servers.CreateOpts{
Name: serverName,
ImageRef: image.ID,
FlavorRef: flavor.ID,
Networks: []servers.Network{
{
Port: fi.StringValue(e.Port.ID),
},
},
Metadata: e.Metadata,
SecurityGroups: e.SecurityGroups,
}
if e.UserData != nil {
bytes, err := fi.ResourceAsBytes(e.UserData)
if err != nil {
return err
}
opt.UserData = bytes
}
if e.AvailabilityZone != nil {
opt.AvailabilityZone = fi.StringValue(e.AvailabilityZone)
}
keyext := keypairs.CreateOptsExt{
CreateOptsBuilder: opt,
KeyName: openstackKeyPairName(fi.StringValue(e.SSHKey)),
}
sgext := schedulerhints.CreateOptsExt{
CreateOptsBuilder: keyext,
SchedulerHints: &schedulerhints.SchedulerHints{
Group: *e.ServerGroup.ID,
},
}
opts, err := includeBootVolumeOptions(t, e, sgext)
if err != nil {
return err
}
v, err := t.Cloud.CreateInstance(opts, fi.StringValue(e.Port.ID))
if err != nil {
return fmt.Errorf("Error creating instance: %v", err)
}
e.ID = fi.String(v.ID)
e.ServerGroup.AddNewMember(fi.StringValue(e.ID))
if e.FloatingIP != nil {
err = associateFloatingIP(t, e)
if err != nil {
return err
}
}
klog.V(2).Infof("Creating a new Openstack instance, id=%s", v.ID)
return nil
}
if changes.Port != nil {
ports.Update(cloud.NetworkingClient(), fi.StringValue(changes.Port.ID), ports.UpdateOpts{
DeviceID: e.ID,
})
}
if changes.FloatingIP != nil {
err := associateFloatingIP(t, e)
if err != nil {
return err
}
}
return nil
}
func associateFloatingIP(t *openstack.OpenstackAPITarget, e *Instance) error {
cloud := t.Cloud.(openstack.OpenstackCloud)
client := cloud.NetworkingClient()
_, err := l3floatingip.Update(client, fi.StringValue(e.FloatingIP.ID), l3floatingip.UpdateOpts{
PortID: e.Port.ID,
}).Extract()
if err != nil {
return fmt.Errorf("failed to associated floating IP to instance %s: %v", *e.Name, err)
}
return nil
}
func includeBootVolumeOptions(t *openstack.OpenstackAPITarget, e *Instance, opts servers.CreateOptsBuilder) (servers.CreateOptsBuilder, error) {
if !bootFromVolume(e.Metadata) {
return opts, nil
}
i, err := t.Cloud.GetImage(fi.StringValue(e.Image))
if err != nil {
return nil, fmt.Errorf("Error getting image information: %v", err)
}
bfv := bootfromvolume.CreateOptsExt{
CreateOptsBuilder: opts,
BlockDevice: []bootfromvolume.BlockDevice{{
BootIndex: 0,
DeleteOnTermination: true,
DestinationType: "volume",
SourceType: "image",
UUID: i.ID,
VolumeSize: i.MinDiskGigabytes,
}},
}
if s, ok := e.Metadata[openstack.BOOT_VOLUME_SIZE]; ok {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return nil, fmt.Errorf("Invalid value for %v: %v", openstack.BOOT_VOLUME_SIZE, err)
}
bfv.BlockDevice[0].VolumeSize = int(i)
}
return bfv, nil
}
func bootFromVolume(m map[string]string) bool {
v, ok := m[openstack.BOOT_FROM_VOLUME]
if !ok {
return false
}
switch v {
case "true", "enabled":
return true
default:
return false
}
}