func (p *ironicProvisioner) PreprovisioningImageFormats() ([]metal3v1alpha1.ImageFormat, error) {
	if !p.config.havePreprovImgBuilder {
		return nil, nil
	}
	accessDetails, err := p.bmcAccess()
	if err != nil {
		return nil, err
	}
	var formats []metal3v1alpha1.ImageFormat
	if accessDetails.SupportsISOPreprovisioningImage() {
		formats = append(formats, metal3v1alpha1.ImageFormatISO)
	} else {
		if p.config.deployKernelURL != "" && p.config.deployRamdiskURL != "" {
			return nil, nil
		}
	}
	if p.config.deployKernelURL != "" {
		formats = append(formats, metal3v1alpha1.ImageFormatInitRD)
	}
	return formats, nil
}
PreprovisioningImageFormats returns a list of acceptable formats for a
pre-provisioning image to be built by a PreprovisioningImage object. The
list should be nil if no image build is requested.
package ironic
import (
	"fmt"
	"strings"
	"time"
	"github.com/metal3-io/baremetal-operator/pkg/ironic"
	"github.com/metal3-io/baremetal-operator/pkg/ironic/devicehints"
	"github.com/metal3-io/baremetal-operator/pkg/ironic/hardwaredetails"
	"github.com/metal3-io/baremetal-operator/pkg/ironic/bmc"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"github.com/go-logr/logr"
	"github.com/gophercloud/gophercloud"
	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/ports"
	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
	"github.com/pkg/errors"
	"sigs.k8s.io/yaml"
	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
	"github.com/metal3-io/baremetal-operator/pkg/provisioner"
)
var (
	deprovisionRequeueDelay   = time.Second * 10
	provisionRequeueDelay     = time.Second * 10
	powerRequeueDelay         = time.Second * 10
	introspectionRequeueDelay = time.Second * 15
	softPowerOffTimeout       = time.Second * 180
)
const (
	powerOn              = string(nodes.PowerOn)
	powerOff             = string(nodes.PowerOff)
	softPowerOff         = string(nodes.SoftPowerOff)
	powerNone            = "None"
	nameSeparator        = "~"
	customDeployPriority = 80
	deployKernelKey  = "deploy_kernel"
	deployRamdiskKey = "deploy_ramdisk"
	deployISOKey     = "deploy_iso"
)
var bootModeCapabilities = map[metal3v1alpha1.BootMode]string{
	metal3v1alpha1.UEFI:           "boot_mode:uefi",
	metal3v1alpha1.UEFISecureBoot: "boot_mode:uefi,secure_boot:true",
	metal3v1alpha1.Legacy:         "boot_mode:bios",
}
type macAddressConflictError struct {
	Address      string
	ExistingNode string
}
func (e macAddressConflictError) Error() string {
	return fmt.Sprintf("MAC address %s conflicts with existing node %s", e.Address, e.ExistingNode)
}
func NewMacAddressConflictError(address, node string) error {
	return macAddressConflictError{Address: address, ExistingNode: node}
}
type ironicConfig struct {
	havePreprovImgBuilder            bool
	deployKernelURL                  string
	deployRamdiskURL                 string
	deployISOURL                     string
	liveISOForcePersistentBootDevice string
	maxBusyHosts                     int
}
type ironicProvisioner struct {
	config ironicConfig
	objectMeta metav1.ObjectMeta
	nodeID string
	bmcAddress string
	disableCertVerification bool
	bmcCreds bmc.Credentials
	bootMACAddress string
	client *gophercloud.ServiceClient
	inspector *gophercloud.ServiceClient
	log logr.Logger
	debugLog logr.Logger
	publisher provisioner.EventPublisher
}
func (p *ironicProvisioner) bmcAccess() (bmc.AccessDetails, error) {
	bmcAccess, err := bmc.NewAccessDetails(p.bmcAddress, p.disableCertVerification)
	if err != nil {
		return nil, errors.Wrap(err, "failed to parse BMC address information")
	}
	return bmcAccess, nil
}
func (p *ironicProvisioner) validateNode(ironicNode *nodes.Node) (errorMessage string, err error) {
	var validationErrors []string
	p.log.Info("validating node settings in ironic")
	validateResult, err := nodes.Validate(p.client, ironicNode.UUID).Extract()
	if err != nil {
		return "", err  
	}
	if !validateResult.Boot.Result {
		validationErrors = append(validationErrors, validateResult.Boot.Reason)
	}
	if !validateResult.Deploy.Result {
		validationErrors = append(validationErrors, validateResult.Deploy.Reason)
	}
	if len(validationErrors) > 0 {
		errorMessage = fmt.Sprintf("host validation error: %s",
			strings.Join(validationErrors, "; "))
		return errorMessage, nil
	}
	return "", nil
}
func (p *ironicProvisioner) listAllPorts(address string) ([]ports.Port, error) {
	var allPorts []ports.Port
	opts := ports.ListOpts{
		Fields: []string{"node_uuid"},
	}
	if address != "" {
		opts.Address = address
	}
	pager := ports.List(p.client, opts)
	allPages, err := pager.AllPages()
	if err != nil {
		return allPorts, err
	}
	return ports.ExtractPorts(allPages)
}
func (p *ironicProvisioner) getNode() (*nodes.Node, error) {
	if p.nodeID == "" {
		return nil, provisioner.ErrNeedsRegistration
	}
	ironicNode, err := nodes.Get(p.client, p.nodeID).Extract()
	switch err.(type) {
	case nil:
		p.debugLog.Info("found existing node by ID")
		return ironicNode, nil
	case gophercloud.ErrDefault404:
		return nil, provisioner.ErrNeedsRegistration
	default:
		return nil, errors.Wrap(err,
			fmt.Sprintf("failed to find node by ID %s", p.nodeID))
	}
}
func (p *ironicProvisioner) nodeHasAssignedPort(ironicNode *nodes.Node) (bool, error) {
	opts := ports.ListOpts{
		Fields:   []string{"node_uuid"},
		NodeUUID: ironicNode.UUID,
	}
	pager := ports.List(p.client, opts)
	allPages, err := pager.AllPages()
	if err != nil {
		return false, errors.Wrap(err, "failed to page over list of ports")
	}
	empty, err := allPages.IsEmpty()
	if err != nil {
		return false, errors.Wrap(err, "failed to check port list status")
	}
	if empty {
		p.debugLog.Info("node has no assigned port", "node", ironicNode.UUID)
		return false, nil
	}
	p.debugLog.Info("node has assigned port", "node", ironicNode.UUID)
	return true, nil
}
func (p *ironicProvisioner) isAddressAllocatedToPort(address string) (bool, error) {
	allPorts, err := p.listAllPorts(address)
	if err != nil {
		return false, errors.Wrap(err, fmt.Sprintf("failed to list ports for %s", address))
	}
	if len(allPorts) == 0 {
		p.debugLog.Info("address does not have allocated ports", "address", address)
		return false, nil
	}
	p.debugLog.Info("address is allocated to port", "address", address, "node", allPorts[0].NodeUUID)
	return true, nil
}
func (p *ironicProvisioner) findExistingHost(bootMACAddress string) (ironicNode *nodes.Node, err error) {
	ironicNode, err = p.getNode()
	if !errors.Is(err, provisioner.ErrNeedsRegistration) {
		return
	}
	nodeSearchList := []string{ironicNodeName(p.objectMeta)}
	if !strings.Contains(p.objectMeta.Name, nameSeparator) {
		nodeSearchList = append(nodeSearchList, p.objectMeta.Name)
	}
	for _, nodeName := range nodeSearchList {
		p.debugLog.Info("looking for existing node by name", "name", nodeName)
		ironicNode, err = nodes.Get(p.client, nodeName).Extract()
		switch err.(type) {
		case nil:
			p.debugLog.Info("found existing node by name", "name", nodeName, "node", ironicNode.UUID)
			return ironicNode, nil
		case gophercloud.ErrDefault404:
			p.log.Info(
				fmt.Sprintf("node with name %s doesn't exist", nodeName))
		default:
			return nil, errors.Wrap(err,
				fmt.Sprintf("failed to find node by name %s", nodeName))
		}
	}
	p.log.Info("looking for existing node by MAC", "MAC", bootMACAddress)
	allPorts, err := p.listAllPorts(bootMACAddress)
	if err != nil {
		p.log.Info("failed to find an existing port with address", "MAC", bootMACAddress)
		return nil, nil
	}
	if len(allPorts) > 0 {
		nodeUUID := allPorts[0].NodeUUID
		ironicNode, err = nodes.Get(p.client, nodeUUID).Extract()
		switch err.(type) {
		case nil:
			p.debugLog.Info("found existing node by MAC", "MAC", bootMACAddress, "node", ironicNode.UUID, "name", ironicNode.Name)
			if ironicNode.Name != "" {
				return nil, NewMacAddressConflictError(bootMACAddress, ironicNode.Name)
			}
			return ironicNode, nil
		case gophercloud.ErrDefault404:
			return nil, errors.Wrap(err,
				fmt.Sprintf("port %s exists but linked node doesn't %s", bootMACAddress, nodeUUID))
		default:
			return nil, errors.Wrap(err,
				fmt.Sprintf("port %s exists but failed to find linked node by ID %s", bootMACAddress, nodeUUID))
		}
	} else {
		p.log.Info("port with address doesn't exist", "MAC", bootMACAddress)
	}
	return nil, nil
}
func (p *ironicProvisioner) createPXEEnabledNodePort(uuid, macAddress string) error {
	p.log.Info("creating PXE enabled ironic port for node", "NodeUUID", uuid, "MAC", macAddress)
	enable := true
	_, err := ports.Create(
		p.client,
		ports.CreateOpts{
			NodeUUID:   uuid,
			Address:    macAddress,
			PXEEnabled: &enable,
		}).Extract()
	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("failed to create ironic port for node: %s, MAC: %s", uuid, macAddress))
	}
	return nil
}
func (p *ironicProvisioner) ValidateManagementAccess(data provisioner.ManagementAccessData, credentialsChanged, force bool) (result provisioner.Result, provID string, err error) {
	bmcAccess, err := p.bmcAccess()
	if err != nil {
		result, err = operationFailed(err.Error())
		return
	}
	var ironicNode *nodes.Node
	updater := updateOptsBuilder(p.debugLog)
	p.debugLog.Info("validating management access")
	ironicNode, err = p.findExistingHost(p.bootMACAddress)
	if err != nil {
		switch err.(type) {
		case macAddressConflictError:
			result, err = operationFailed(err.Error())
		default:
			result, err = transientError(errors.Wrap(err, "failed to find existing host"))
		}
		return
	}
	if bmcAccess.NeedsMAC() && p.bootMACAddress == "" {
		msg := fmt.Sprintf("BMC driver %s requires a BootMACAddress value", bmcAccess.Type())
		p.log.Info(msg)
		result, err = operationFailed(msg)
		return
	}
	driverInfo := bmcAccess.DriverInfo(p.bmcCreds)
	deployImageInfo := setDeployImage(driverInfo, p.config, bmcAccess, data.PreprovisioningImage)
	if ironicNode == nil {
		p.log.Info("registering host in ironic")
		if data.BootMode == metal3v1alpha1.UEFISecureBoot && !bmcAccess.SupportsSecureBoot() {
			msg := fmt.Sprintf("BMC driver %s does not support secure boot", bmcAccess.Type())
			p.log.Info(msg)
			result, err = operationFailed(msg)
			return
		}
		ironicNode, err = nodes.Create(
			p.client,
			nodes.CreateOpts{
				Driver:              bmcAccess.Driver(),
				BIOSInterface:       bmcAccess.BIOSInterface(),
				BootInterface:       bmcAccess.BootInterface(),
				Name:                p.objectMeta.Name,
				DriverInfo:          driverInfo,
				DeployInterface:     p.deployInterface(data),
				InspectInterface:    "inspector",
				ManagementInterface: bmcAccess.ManagementInterface(),
				PowerInterface:      bmcAccess.PowerInterface(),
				RAIDInterface:       bmcAccess.RAIDInterface(),
				VendorInterface:     bmcAccess.VendorInterface(),
				Properties: map[string]interface{}{
					"capabilities": bootModeCapabilities[data.BootMode],
				},
			}).Extract()
		if err != nil {
			result, err = transientError(errors.Wrap(err, "failed to register host in ironic"))
			return
		}
		p.publisher("Registered", "Registered new host")
		provID = ironicNode.UUID
		if p.bootMACAddress != "" {
			err = p.createPXEEnabledNodePort(ironicNode.UUID, p.bootMACAddress)
			if err != nil {
				result, err = transientError(err)
				return
			}
		}
	} else {
		provID = ironicNode.UUID
		updater.SetTopLevelOpt("name", ironicNodeName(p.objectMeta), ironicNode.Name)
		if p.bootMACAddress != "" {
			var nodeHasAssignedPort, addressIsAllocatedToPort bool
			nodeHasAssignedPort, err = p.nodeHasAssignedPort(ironicNode)
			if err != nil {
				result, err = transientError(err)
				return
			}
			if !nodeHasAssignedPort {
				addressIsAllocatedToPort, err = p.isAddressAllocatedToPort(p.bootMACAddress)
				if err != nil {
					result, err = transientError(err)
					return
				}
				if !addressIsAllocatedToPort {
					err = p.createPXEEnabledNodePort(ironicNode.UUID, p.bootMACAddress)
					if err != nil {
						result, err = transientError(err)
						return
					}
				}
			}
		}
		if credentialsChanged {
			updater.SetTopLevelOpt("driver_info", driverInfo, ironicNode.DriverInfo)
		} else {
			updater.SetDriverInfoOpts(deployImageInfo, ironicNode)
		}
	}
	if data.CurrentImage != nil || data.HasCustomDeploy {
		p.getImageUpdateOptsForNode(ironicNode, data.CurrentImage, data.BootMode, data.HasCustomDeploy, updater)
	}
	updater.SetTopLevelOpt("automated_clean",
		data.AutomatedCleaningMode != metal3v1alpha1.CleaningModeDisabled,
		ironicNode.AutomatedClean)
	var success bool
	success, result, err = p.tryUpdateNode(ironicNode, updater)
	if !success {
		return
	}
	p.log.Info("current provision state",
		"lastError", ironicNode.LastError,
		"current", ironicNode.ProvisionState,
		"target", ironicNode.TargetProvisionState,
	)
	result, err = operationComplete()
	switch nodes.ProvisionState(ironicNode.ProvisionState) {
	case nodes.Enroll:
		if ironicNode.LastError != "" && !(credentialsChanged || force) {
			result, err = operationFailed(ironicNode.LastError)
			return
		}
		if ironicNode.TargetProvisionState == string(nodes.TargetManage) {
			result, err = operationContinuing(provisionRequeueDelay)
			return
		}
		result, err = p.changeNodeProvisionState(
			ironicNode,
			nodes.ProvisionStateOpts{Target: nodes.TargetManage},
		)
		return
	case nodes.Verifying:
		result, err = operationContinuing(provisionRequeueDelay)
		return
	case nodes.Active:
		p.debugLog.Info("have active host", "image_source", ironicNode.InstanceInfo["image_source"])
		fallthrough
	case nodes.Manageable:
		fallthrough
	case nodes.Available:
		fallthrough
	default:
		switch data.State {
		case metal3v1alpha1.StateInspecting,
			metal3v1alpha1.StatePreparing,
			metal3v1alpha1.StateProvisioning,
			metal3v1alpha1.StateDeprovisioning:
			if deployImageInfo == nil {
				if p.config.havePreprovImgBuilder {
					result, err = transientError(provisioner.ErrNeedsPreprovisioningImage)
				} else {
					result, err = operationFailed("no preprovisioning image available")
				}
			}
		}
		return
	}
}