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

470 lines
12 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 (
"context"
"fmt"
"strconv"
"strings"
l3floatingip "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports"
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces"
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/keypairs"
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
"k8s.io/klog/v2"
"k8s.io/kops/pkg/truncate"
"k8s.io/kops/pkg/wellknownservices"
"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
ConfigDrive *bool
Status *string
Lifecycle fi.Lifecycle
// WellKnownServices indicates which services are supported by this resource.
// This field is internal and is not rendered to the cloud.
WellKnownServices []wellknownservices.WellKnownService
}
var (
_ fi.CloudupTask = &Instance{}
_ fi.HasAddress = &Instance{}
_ fi.CloudupHasDependencies = &Instance{}
)
// Constants for truncating Tags
const MAX_TAG_LENGTH_OPENSTACK = 60
var TRUNCATE_OPT = truncate.TruncateStringOptions{
MaxLength: MAX_TAG_LENGTH_OPENSTACK,
AlwaysAddHash: false,
HashLength: 6,
}
// GetDependencies returns the dependencies of the Instance task
func (e *Instance) GetDependencies(tasks map[string]fi.CloudupTask) []fi.CloudupTask {
var deps []fi.CloudupTask
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) CompareWithID() *string {
return e.ID
}
// GetWellKnownServices implements fi.HasAddress::GetWellKnownServices.
// It indicates which services we support with this instance.
func (e *Instance) GetWellKnownServices() []wellknownservices.WellKnownService {
return e.WellKnownServices
}
func (e *Instance) FindAddresses(context *fi.CloudupContext) ([]string, error) {
cloud := context.T.Cloud.(openstack.OpenstackCloud)
if e.Port == nil {
return nil, nil
}
ports, err := cloud.GetPort(fi.ValueOf(e.Port.ID))
if err != nil {
return nil, err
}
for _, port := range ports.FixedIPs {
return []string{port.IPAddress}, nil
}
return nil, nil
}
// filterInstancePorts tries to get all ports of an instance tagged with the cluster name.
// If no tagged ports are found it will return all ports of the instance, to not change the legacy behavior when there weren't tagged ports
func filterInstancePorts(allPorts []ports.Port, clusterName string) []ports.Port {
clusterNameTag := truncate.TruncateString(fmt.Sprintf("%s=%s", openstack.TagClusterName, clusterName), TRUNCATE_OPT)
var taggedPorts []ports.Port
for _, port := range allPorts {
for _, tag := range port.Tags {
if tag == clusterNameTag {
taggedPorts = append(taggedPorts, port)
break
}
}
}
if len(taggedPorts) == 0 {
return allPorts
}
return taggedPorts
}
func (e *Instance) Find(c *fi.CloudupContext) (*Instance, error) {
if e == nil || e.Name == nil {
return nil, nil
}
cloud := c.T.Cloud.(openstack.OpenstackCloud)
serverList, err := cloud.ListInstances(servers.ListOpts{
Name: fmt.Sprintf("^%s", fi.ValueOf(e.GroupName)),
})
if err != nil {
return nil, fmt.Errorf("error listing servers: %v", err)
}
var filteredList []servers.Server
for _, server := range serverList {
val, ok := server.Metadata["k8s"]
if !ok || val != fi.ValueOf(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.ValueOf(e.Name) || metadataName == fi.ValueOf(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.ValueOf(e.Name))
}
server := filteredList[0]
actual := &Instance{
ID: fi.PtrTo(server.ID),
Name: e.Name,
SSHKey: fi.PtrTo(server.KeyName),
Lifecycle: e.Lifecycle,
Metadata: server.Metadata,
Role: fi.PtrTo(server.Metadata["KopsRole"]),
AvailabilityZone: e.AvailabilityZone,
GroupName: e.GroupName,
ConfigDrive: e.ConfigDrive,
Status: fi.PtrTo(server.Status),
}
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)
}
ports = filterInstancePorts(ports, fi.ValueOf(e.ServerGroup.ClusterName))
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.ValueOf(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.PtrTo(fip.ID),
Name: fi.PtrTo(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
e.Status = fi.PtrTo(activeStatus)
actual.WellKnownServices = e.WellKnownServices
// 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.CloudupContext) error {
return fi.CloudupDefaultDeltaRunMethod(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 fi.ValueOf(a.Status) == errorStatus {
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.ValueOf(e.GroupName), hash[0:6])), nil
}
func (_ *Instance) RenderOpenstack(t *openstack.OpenstackAPITarget, a, e, changes *Instance) error {
cloud := t.Cloud
if a != nil && fi.ValueOf(a.Status) == errorStatus {
klog.V(2).Infof("Delete previously failed server: %s\n", fi.ValueOf(a.ID))
cloud.DeleteInstanceWithID(fi.ValueOf(a.ID))
}
if a == nil || fi.ValueOf(a.Status) == errorStatus {
serverName, err := generateInstanceName(e)
if err != nil {
return err
}
klog.V(2).Infof("Creating Instance with name: %q", serverName)
imageName := fi.ValueOf(e.Image)
image, err := cloud.GetImage(imageName)
if err != nil {
return fmt.Errorf("failed to find image %v: %v", imageName, err)
}
flavorName := fi.ValueOf(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.ValueOf(e.Port.ID),
},
},
Metadata: e.Metadata,
SecurityGroups: e.SecurityGroups,
ConfigDrive: e.ConfigDrive,
}
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.ValueOf(e.AvailabilityZone)
}
if opt, err = includeBootVolumeOptions(t, e, opt); err != nil {
return err
}
keyext := keypairs.CreateOptsExt{
CreateOptsBuilder: opt,
KeyName: openstackKeyPairName(fi.ValueOf(e.SSHKey)),
}
schedulerHints := servers.SchedulerHintOpts{Group: *e.ServerGroup.ID}
v, err := t.Cloud.CreateInstance(keyext, schedulerHints, fi.ValueOf(e.Port.ID))
if err != nil {
return fmt.Errorf("Error creating instance: %v", err)
}
e.ID = fi.PtrTo(v.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 {
_, err := attachinterfaces.Create(context.TODO(), cloud.ComputeClient(), fi.ValueOf(e.ID), attachinterfaces.CreateOpts{
PortID: fi.ValueOf(changes.Port.ID),
}).Extract()
if err != nil {
return err
}
}
if changes.FloatingIP != nil {
err := associateFloatingIP(t, e)
if err != nil {
return err
}
}
return nil
}
func associateFloatingIP(t *openstack.OpenstackAPITarget, e *Instance) error {
client := t.Cloud.NetworkingClient()
_, err := l3floatingip.Update(context.TODO(), client, fi.ValueOf(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.CreateOpts) (servers.CreateOpts, error) {
if !bootFromVolume(e.Metadata) {
return opts, nil
}
i, err := t.Cloud.GetImage(fi.ValueOf(e.Image))
if err != nil {
return servers.CreateOpts{}, fmt.Errorf("Error getting image information: %v", err)
}
blockDevice := servers.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 servers.CreateOpts{}, fmt.Errorf("Invalid value for %v: %v", openstack.BOOT_VOLUME_SIZE, err)
}
blockDevice.VolumeSize = int(i)
}
opts.BlockDevice = []servers.BlockDevice{blockDevice}
return opts, 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
}
}