pkg/executables/cmk.go (478 lines of code) (raw):
package executables
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/aws/eks-anywhere/pkg/api/v1alpha1"
"github.com/aws/eks-anywhere/pkg/filewriter"
"github.com/aws/eks-anywhere/pkg/logger"
"github.com/aws/eks-anywhere/pkg/providers/cloudstack/decoder"
"github.com/aws/eks-anywhere/pkg/templater"
)
//go:embed config/cmk.ini
var cmkConfigTemplate string
const (
cmkPath = "cmk"
cmkConfigFileNameTemplate = "cmk_%s.ini"
defaultCloudStackPreflightTimeout = "30"
rootDomain = "ROOT"
domainDelimiter = "/"
)
// Cmk this struct wraps around the CloudMonkey executable CLI to perform operations against a CloudStack endpoint.
type Cmk struct {
writer filewriter.FileWriter
executable Executable
configMap map[string]decoder.CloudStackProfileConfig
}
type listTemplatesResponse struct {
CmkTemplates []cmkTemplate `json:"template"`
}
func (c *Cmk) Close(ctx context.Context) error {
return nil
}
func (c *Cmk) ValidateTemplatePresent(ctx context.Context, profile string, domainId string, zoneId string, account string, template v1alpha1.CloudStackResourceIdentifier) error {
command := newCmkCommand("list templates")
applyCmkArgs(&command, appendArgs("templatefilter=all"), appendArgs("listall=true"))
if len(template.Id) > 0 {
applyCmkArgs(&command, withCloudStackId(template.Id))
} else {
applyCmkArgs(&command, withCloudStackName(template.Name))
}
applyCmkArgs(&command, withCloudStackZoneId(zoneId))
if len(domainId) > 0 {
applyCmkArgs(&command, withCloudStackDomainId(domainId))
if len(account) > 0 {
applyCmkArgs(&command, withCloudStackAccount(account))
}
}
result, err := c.exec(ctx, profile, command...)
if err != nil {
return fmt.Errorf("getting templates info - %s: %v", result.String(), err)
}
if result.Len() == 0 {
return fmt.Errorf("template %s not found", template)
}
response := listTemplatesResponse{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return fmt.Errorf("parsing response into json: %v", err)
}
templates := response.CmkTemplates
if len(templates) > 1 {
return fmt.Errorf("duplicate templates %s found", template)
} else if len(templates) == 0 {
return fmt.Errorf("template %s not found", template)
}
return nil
}
// SearchTemplate looks for a template by name or by id and returns template name if found.
func (c *Cmk) SearchTemplate(ctx context.Context, profile string, template v1alpha1.CloudStackResourceIdentifier) (string, error) {
command := newCmkCommand("list templates")
applyCmkArgs(&command, appendArgs("templatefilter=all"), appendArgs("listall=true"))
if len(template.Id) > 0 {
applyCmkArgs(&command, withCloudStackId(template.Id))
} else {
applyCmkArgs(&command, withCloudStackName(template.Name))
}
result, err := c.exec(ctx, profile, command...)
if err != nil {
return "", fmt.Errorf("getting templates info - %s: %v", result.String(), err)
}
if result.Len() == 0 {
return "", nil
}
response := listTemplatesResponse{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return "", fmt.Errorf("parsing response into json: %v", err)
}
templates := response.CmkTemplates
if len(templates) > 1 {
return "", fmt.Errorf("duplicate templates %s found", template)
} else if len(templates) == 0 {
return "", nil
}
return templates[0].Name, nil
}
func (c *Cmk) ValidateServiceOfferingPresent(ctx context.Context, profile string, zoneId string, serviceOffering v1alpha1.CloudStackResourceIdentifier) error {
command := newCmkCommand("list serviceofferings")
if len(serviceOffering.Id) > 0 {
applyCmkArgs(&command, withCloudStackId(serviceOffering.Id))
} else {
applyCmkArgs(&command, withCloudStackName(serviceOffering.Name))
}
applyCmkArgs(&command, withCloudStackZoneId(zoneId))
result, err := c.exec(ctx, profile, command...)
if err != nil {
return fmt.Errorf("getting service offerings info - %s: %v", result.String(), err)
}
if result.Len() == 0 {
return fmt.Errorf("service offering %s not found", serviceOffering)
}
response := struct {
CmkServiceOfferings []cmkServiceOffering `json:"serviceoffering"`
}{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return fmt.Errorf("parsing response into json: %v", err)
}
offerings := response.CmkServiceOfferings
if len(offerings) > 1 {
return fmt.Errorf("duplicate service offering %s found", serviceOffering)
} else if len(offerings) == 0 {
return fmt.Errorf("service offering %s not found", serviceOffering)
}
return nil
}
func (c *Cmk) ValidateDiskOfferingPresent(ctx context.Context, profile string, zoneId string, diskOffering v1alpha1.CloudStackResourceDiskOffering) error {
command := newCmkCommand("list diskofferings")
if len(diskOffering.Id) > 0 {
applyCmkArgs(&command, withCloudStackId(diskOffering.Id))
} else {
applyCmkArgs(&command, withCloudStackName(diskOffering.Name))
}
applyCmkArgs(&command, withCloudStackZoneId(zoneId))
result, err := c.exec(ctx, profile, command...)
if err != nil {
return fmt.Errorf("getting disk offerings info - %s: %v", result.String(), err)
}
if result.Len() == 0 {
return fmt.Errorf("disk offering ID/Name %s/%s not found", diskOffering.Id, diskOffering.Name)
}
response := struct {
CmkDiskOfferings []cmkDiskOffering `json:"diskoffering"`
}{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return fmt.Errorf("parsing response into json: %v", err)
}
offerings := response.CmkDiskOfferings
if len(offerings) > 1 {
return fmt.Errorf("duplicate disk offering ID/Name %s/%s found", diskOffering.Id, diskOffering.Name)
} else if len(offerings) == 0 {
return fmt.Errorf("disk offering ID/Name %s/%s not found", diskOffering.Id, diskOffering.Name)
}
if offerings[0].Customized && diskOffering.CustomSize <= 0 {
return fmt.Errorf("disk offering size %d <= 0 for customized disk offering", diskOffering.CustomSize)
}
if !offerings[0].Customized && diskOffering.CustomSize > 0 {
return fmt.Errorf("disk offering size %d > 0 for non-customized disk offering", diskOffering.CustomSize)
}
return nil
}
func (c *Cmk) ValidateAffinityGroupsPresent(ctx context.Context, profile string, domainId string, account string, affinityGroupIds []string) error {
for _, affinityGroupId := range affinityGroupIds {
command := newCmkCommand("list affinitygroups")
applyCmkArgs(&command, withCloudStackId(affinityGroupId))
// account must be specified with a domainId
// domainId can be specified without account
if len(domainId) > 0 {
applyCmkArgs(&command, withCloudStackDomainId(domainId))
if len(account) > 0 {
applyCmkArgs(&command, withCloudStackAccount(account))
}
}
result, err := c.exec(ctx, profile, command...)
if err != nil {
return fmt.Errorf("getting affinity group info - %s: %v", result.String(), err)
}
if result.Len() == 0 {
return fmt.Errorf(fmt.Sprintf("affinity group %s not found", affinityGroupId))
}
response := struct {
CmkAffinityGroups []cmkAffinityGroup `json:"affinitygroup"`
}{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return fmt.Errorf("parsing response into json: %v", err)
}
affinityGroup := response.CmkAffinityGroups
if len(affinityGroup) > 1 {
return fmt.Errorf("duplicate affinity group %s found", affinityGroupId)
} else if len(affinityGroup) == 0 {
return fmt.Errorf("affinity group %s not found", affinityGroupId)
}
}
return nil
}
func (c *Cmk) ValidateZoneAndGetId(ctx context.Context, profile string, zone v1alpha1.CloudStackZone) (string, error) {
command := newCmkCommand("list zones")
if len(zone.Id) > 0 {
applyCmkArgs(&command, withCloudStackId(zone.Id))
} else {
applyCmkArgs(&command, withCloudStackName(zone.Name))
}
result, err := c.exec(ctx, profile, command...)
if err != nil {
return "", fmt.Errorf("getting zones info - %s: %v", result.String(), err)
}
if result.Len() == 0 {
return "", fmt.Errorf("zone %s not found", zone)
}
response := struct {
CmkZones []cmkResourceIdentifier `json:"zone"`
}{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return "", fmt.Errorf("parsing response into json: %v", err)
}
cmkZones := response.CmkZones
if len(cmkZones) > 1 {
return "", fmt.Errorf("duplicate zone %s found", zone)
} else if len(cmkZones) == 0 {
return "", fmt.Errorf("zone %s not found", zone)
}
return cmkZones[0].Id, nil
}
func (c *Cmk) ValidateDomainAndGetId(ctx context.Context, profile string, domain string) (string, error) {
domainId := ""
command := newCmkCommand("list domains")
// "list domains" API does not support querying by domain path, so here we extract the domain name which is the last part of the input domain
tokens := strings.Split(domain, domainDelimiter)
domainName := tokens[len(tokens)-1]
applyCmkArgs(&command, withCloudStackName(domainName), appendArgs("listall=true"))
result, err := c.exec(ctx, profile, command...)
if err != nil {
return domainId, fmt.Errorf("getting domain info - %s: %v", result.String(), err)
}
if result.Len() == 0 {
return domainId, fmt.Errorf("domain %s not found", domain)
}
response := struct {
CmkDomains []cmkDomain `json:"domain"`
}{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return domainId, fmt.Errorf("parsing response into json: %v", err)
}
domains := response.CmkDomains
var domainPath string
if domain == rootDomain {
domainPath = rootDomain
} else {
domainPath = strings.Join([]string{rootDomain, domain}, domainDelimiter)
}
for _, d := range domains {
if d.Path == domainPath {
domainId = d.Id
break
}
}
if domainId == "" {
return domainId, fmt.Errorf("domain(s) found for domain name %s, but not found a domain with domain path %s", domain, domainPath)
}
return domainId, nil
}
// EnsureNoDuplicateNetwork ensures that there are no duplicate networks with the name networkName.
// If it finds duplicates that are not shared networks, it deletes them.
func (c *Cmk) EnsureNoDuplicateNetwork(ctx context.Context, profile string, networkName string) error {
command := newCmkCommand(fmt.Sprintf("list networks filter=name,id,type keyword=%s", networkName))
result, err := c.exec(ctx, profile, command...)
if err != nil {
return fmt.Errorf("getting network info - %s: %v", result.String(), err)
}
response := struct {
CmkNetworks []cmkNetwork `json:"network"`
}{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return fmt.Errorf("parsing response into json: %v", err)
}
for _, network := range response.CmkNetworks {
if !strings.EqualFold(network.Type, "Shared") {
command := newCmkCommand(fmt.Sprintf("delete network id=%s force=true", network.ID))
result, err := c.exec(ctx, profile, command...)
if err != nil {
return fmt.Errorf("deleting duplicate network with ID %s - %s: %v", network, result.String(), err)
}
}
}
return nil
}
func (c *Cmk) ValidateNetworkPresent(ctx context.Context, profile string, domainId string, network v1alpha1.CloudStackResourceIdentifier, zoneId string, account string) error {
command := newCmkCommand("list networks")
// account must be specified within a domainId
// domainId can be specified without account
if len(domainId) > 0 {
applyCmkArgs(&command, withCloudStackDomainId(domainId))
if len(account) > 0 {
applyCmkArgs(&command, withCloudStackAccount(account))
}
}
applyCmkArgs(&command, withCloudStackZoneId(zoneId))
result, err := c.exec(ctx, profile, command...)
if err != nil {
return fmt.Errorf("getting network info - %s: %v", result.String(), err)
}
if result.Len() == 0 {
return fmt.Errorf("network %s not found in zone %s", network, zoneId)
}
response := struct {
CmkNetworks []cmkResourceIdentifier `json:"network"`
}{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return fmt.Errorf("parsing response into json: %v", err)
}
networks := response.CmkNetworks
// filter by network name -- cmk does not support name= filter
// if network id and name are both provided, the following code is to confirm name matches return value retrieved by id.
// if only name is provided, the following code is to only get networks with specified name.
if len(network.Name) > 0 {
networks = []cmkResourceIdentifier{}
for _, net := range response.CmkNetworks {
if net.Name == network.Name {
networks = append(networks, net)
}
}
}
if len(networks) > 1 {
return fmt.Errorf("duplicate network %s found", network)
} else if len(networks) == 0 {
return fmt.Errorf("network %s not found in zoneRef %s", network, zoneId)
}
return nil
}
func (c *Cmk) ValidateAccountPresent(ctx context.Context, profile string, account string, domainId string) error {
// If account is not specified then no need to check its presence
if len(account) == 0 {
return nil
}
command := newCmkCommand("list accounts")
applyCmkArgs(&command, withCloudStackName(account), withCloudStackDomainId(domainId))
result, err := c.exec(ctx, profile, command...)
if err != nil {
return fmt.Errorf("getting accounts info - %s: %v", result.String(), err)
}
if result.Len() == 0 {
return fmt.Errorf("account %s not found", account)
}
response := struct {
CmkAccounts []cmkAccount `json:"account"`
}{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return fmt.Errorf("parsing response into json: %v", err)
}
accounts := response.CmkAccounts
if len(accounts) > 1 {
return fmt.Errorf("duplicate account %s found", account)
} else if len(accounts) == 0 {
return fmt.Errorf("account %s not found", account)
}
return nil
}
// NewCmk initializes CloudMonkey executable to query CloudStack via CLI.
func NewCmk(executable Executable, writer filewriter.FileWriter, config *decoder.CloudStackExecConfig) (*Cmk, error) {
if config == nil {
return nil, fmt.Errorf("nil exec config for CloudMonkey, unable to proceed")
}
configMap := make(map[string]decoder.CloudStackProfileConfig, len(config.Profiles))
for _, profile := range config.Profiles {
configMap[profile.Name] = profile
}
return &Cmk{
writer: writer,
executable: executable,
configMap: configMap,
}, nil
}
func (c *Cmk) GetManagementApiEndpoint(profile string) (string, error) {
config, exist := c.configMap[profile]
if exist {
return config.ManagementUrl, nil
}
return "", fmt.Errorf("profile %s does not exist", profile)
}
func (c *Cmk) CleanupVms(ctx context.Context, profile string, clusterName string, dryRun bool) error {
command := newCmkCommand("list virtualmachines")
applyCmkArgs(&command, withCloudStackKeyword(clusterName), appendArgs("listall=true"))
result, err := c.exec(ctx, profile, command...)
if err != nil {
return fmt.Errorf("listing virtual machines in cluster %s: %s: %v", clusterName, result.String(), err)
}
if result.Len() == 0 {
logger.Info("virtual machines not found", "cluster", clusterName)
return nil
}
response := struct {
CmkVirtualMachines []cmkResourceIdentifier `json:"virtualmachine"`
}{}
if err = json.Unmarshal(result.Bytes(), &response); err != nil {
return fmt.Errorf("parsing response into json: %v", err)
}
for _, vm := range response.CmkVirtualMachines {
if dryRun {
logger.Info("Found ", "vm_name", vm.Name)
continue
}
stopCommand := newCmkCommand("stop virtualmachine")
applyCmkArgs(&stopCommand, withCloudStackId(vm.Id), appendArgs("forced=true"))
stopResult, err := c.exec(ctx, profile, stopCommand...)
if err != nil {
return fmt.Errorf("stopping virtual machine with name %s and id %s: %s: %v", vm.Name, vm.Id, stopResult.String(), err)
}
destroyCommand := newCmkCommand("destroy virtualmachine")
applyCmkArgs(&destroyCommand, withCloudStackId(vm.Id), appendArgs("expunge=true"))
destroyResult, err := c.exec(ctx, profile, destroyCommand...)
if err != nil {
return fmt.Errorf("destroying virtual machine with name %s and id %s: %s: %v", vm.Name, vm.Id, destroyResult.String(), err)
}
logger.Info("Deleted ", "vm_name", vm.Name, "vm_id", vm.Id)
}
return nil
}
func (c *Cmk) exec(ctx context.Context, profile string, args ...string) (stdout bytes.Buffer, err error) {
configFile, err := c.buildCmkConfigFile(profile)
if err != nil {
return bytes.Buffer{}, fmt.Errorf("failed cmk validations: %v", err)
}
argsWithConfigFile := append([]string{"-c", configFile}, args...)
return c.executable.Execute(ctx, argsWithConfigFile...)
}
func (c *Cmk) buildCmkConfigFile(profile string) (configFile string, err error) {
config, exist := c.configMap[profile]
if !exist {
return "", fmt.Errorf("profile %s does not exist", profile)
}
t := templater.New(c.writer)
config.Timeout = defaultCloudStackPreflightTimeout
if timeout, isSet := os.LookupEnv("CLOUDSTACK_PREFLIGHT_TIMEOUT"); isSet {
if _, err := strconv.ParseUint(timeout, 10, 16); err != nil {
return "", fmt.Errorf("CLOUDSTACK_PREFLIGHT_TIMEOUT must be a number: %v", err)
}
config.Timeout = timeout
}
writtenFileName, err := t.WriteToFile(cmkConfigTemplate, config, fmt.Sprintf(cmkConfigFileNameTemplate, profile))
if err != nil {
return "", fmt.Errorf("creating file for cmk config: %v", err)
}
configFile, err = filepath.Abs(writtenFileName)
if err != nil {
return "", fmt.Errorf("failed to generate absolute filepath for generated config file at %s", writtenFileName)
}
return configFile, nil
}
type cmkTemplate struct {
Id string `json:"id"`
Name string `json:"name"`
Zonename string `json:"zonename"`
}
type cmkServiceOffering struct {
CpuNumber int `json:"cpunumber"`
CpuSpeed int `json:"cpuspeed"`
Memory int `json:"memory"`
Id string `json:"id"`
Name string `json:"name"`
}
type cmkNetwork struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
type cmkResourceIdentifier struct {
Id string `json:"id"`
Name string `json:"name"`
}
type cmkDiskOffering struct {
Id string `json:"id"`
Name string `json:"name"`
Customized bool `json:"iscustomized"`
}
type cmkAffinityGroup struct {
Type string `json:"type"`
Id string `json:"id"`
Name string `json:"name"`
}
type cmkDomain struct {
Id string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
}
type cmkAccount struct {
RoleType string `json:"roletype"`
Domain string `json:"domain"`
Id string `json:"id"`
Name string `json:"name"`
}