Skip to content

Commit

Permalink
Add live-iso support to ironic provisioner
Browse files Browse the repository at this point in the history
This allows iso images to be booted via the Ironic ramdisk deploy
interface, e.g:

  image:
    url: http://172.22.0.1/images/fedora-coreos-33.20201214.2.0-live.x86_64.iso
    format: live-iso
  • Loading branch information
Steven Hardy committed Jan 12, 2021
1 parent 73f9548 commit de55f31
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 16 deletions.
103 changes: 87 additions & 16 deletions pkg/provisioner/ironic/ironic.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ func (p *ironicProvisioner) ValidateManagementAccess(credentialsChanged bool) (r
BootInterface: p.bmcAccess.BootInterface(),
Name: p.host.Name,
DriverInfo: driverInfo,
DeployInterface: p.deployInterface(),
InspectInterface: "inspector",
ManagementInterface: p.bmcAccess.ManagementInterface(),
PowerInterface: p.bmcAccess.PowerInterface(),
Expand Down Expand Up @@ -421,7 +422,7 @@ func (p *ironicProvisioner) ValidateManagementAccess(credentialsChanged bool) (r
}

_, _, ok := imageData.GetChecksum()
if ok {
if ok || (imageData.DiskFormat != nil && *imageData.DiskFormat == "live-iso") {
updates, err := p.getImageUpdateOptsForNode(ironicNode, imageData)
if err != nil {
return result, errors.Wrap(err, "Could not get Image options for node")
Expand Down Expand Up @@ -732,8 +733,59 @@ func (p *ironicProvisioner) UpdateHardwareState() (result provisioner.Result, er
}

func (p *ironicProvisioner) getImageUpdateOptsForNode(ironicNode *nodes.Node, imageData *metal3v1alpha1.Image) (updates nodes.UpdateOpts, err error) {
// image_source
var op nodes.UpdateOp
if imageData.DiskFormat != nil && *imageData.DiskFormat == "live-iso" {
if _, ok := ironicNode.InstanceInfo["boot_iso"]; !ok {
op = nodes.AddOp
p.log.Info("adding boot_iso")
} else {
op = nodes.ReplaceOp
p.log.Info("updating boot_iso")
}
updates = append(
updates,
nodes.UpdateOperation{
Op: op,
Path: "/instance_info/boot_iso",
Value: imageData.URL,
},
)
updates = append(
updates,
nodes.UpdateOperation{
Op: nodes.ReplaceOp,
Path: "/deploy_interface",
Value: "ramdisk",
},
)
// remove any image_source or checksum options
removals := []string{"image_source", "image_os_hash_checksum", "image_os_hash_algo", "image_checksum"}
op = nodes.RemoveOp
for _, item := range removals {
if _, ok := ironicNode.InstanceInfo[item]; ok {
p.log.Info("removing " + item)
updates = append(
updates,
nodes.UpdateOperation{
Op: op,
Path: "/instance_info/" + item,
},
)
}
}
return updates, nil
}

// Set deploy_interface direct when not booting a live-iso
updates = append(
updates,
nodes.UpdateOperation{
Op: nodes.ReplaceOp,
Path: "/deploy_interface",
Value: "direct",
},
)
// image_source
if _, ok := ironicNode.InstanceInfo["image_source"]; !ok {
op = nodes.AddOp
p.log.Info("adding image_source")
Expand Down Expand Up @@ -1050,6 +1102,14 @@ func (p *ironicProvisioner) setUpForProvisioning(ironicNode *nodes.Node, hostCon
return
}

func (p *ironicProvisioner) deployInterface() (result string) {
result = "direct"
if p.host.Spec.Image != nil && p.host.Spec.Image.DiskFormat != nil && *p.host.Spec.Image.DiskFormat == "live-iso" {
result = "ramdisk"
}
return result
}

// Adopt allows an externally-provisioned server to be adopted by Ironic.
func (p *ironicProvisioner) Adopt(force bool) (result provisioner.Result, err error) {
var ironicNode *nodes.Node
Expand Down Expand Up @@ -1095,6 +1155,30 @@ func (p *ironicProvisioner) Adopt(force bool) (result provisioner.Result, err er
return
}

func (p *ironicProvisioner) ironicHasSameImage(ironicNode *nodes.Node) (sameImage bool) {
// To make it easier to test if ironic is configured with
// the same image we are trying to provision to the host.
if p.host.Spec.Image != nil && p.host.Spec.Image.DiskFormat != nil && *p.host.Spec.Image.DiskFormat == "live-iso" {
sameImage = (ironicNode.InstanceInfo["boot_iso"] == p.host.Spec.Image.URL)
p.log.Info("checking image settings",
"boot_iso", ironicNode.InstanceInfo["boot_iso"],
"same", sameImage,
"provisionState", ironicNode.ProvisionState)
} else {
checksum, checksumType, _ := p.host.GetImageChecksum()
sameImage = (ironicNode.InstanceInfo["image_source"] == p.host.Spec.Image.URL &&
ironicNode.InstanceInfo["image_os_hash_algo"] == checksumType &&
ironicNode.InstanceInfo["image_os_hash_value"] == checksum)
p.log.Info("checking image settings",
"source", ironicNode.InstanceInfo["image_source"],
"image_os_hash_algo", checksumType,
"image_os_has_value", checksum,
"same", sameImage,
"provisionState", ironicNode.ProvisionState)
}
return sameImage
}

// Provision writes the image from the host spec to the host. It may
// be called multiple times, and should return true for its dirty flag
// until the deprovisioning operation is completed.
Expand All @@ -1110,20 +1194,7 @@ func (p *ironicProvisioner) Provision(hostConf provisioner.HostConfigData) (resu

p.log.Info("provisioning image to host", "state", ironicNode.ProvisionState)

checksum, checksumType, _ := p.host.GetImageChecksum()

// Local variable to make it easier to test if ironic is
// configured with the same image we are trying to provision to
// the host.
ironicHasSameImage := (ironicNode.InstanceInfo["image_source"] == p.host.Spec.Image.URL &&
ironicNode.InstanceInfo["image_os_hash_algo"] == checksumType &&
ironicNode.InstanceInfo["image_os_hash_value"] == checksum)
p.log.Info("checking image settings",
"source", ironicNode.InstanceInfo["image_source"],
"image_os_hash_algo", checksumType,
"image_os_has_value", checksum,
"same", ironicHasSameImage,
"provisionState", ironicNode.ProvisionState)
ironicHasSameImage := p.ironicHasSameImage(ironicNode)

result.RequeueAfter = provisionRequeueDelay

Expand Down
7 changes: 7 additions & 0 deletions pkg/provisioner/ironic/ironic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,12 @@ func makeHost() *metal3v1alpha1.BareMetalHost {
}
}

func makeHostLiveIso() (host *metal3v1alpha1.BareMetalHost) {
host = makeHost()
format := "live-iso"
host.Spec.Image.DiskFormat = &format
return host
}

// Implements provisioner.EventPublisher to swallow events for tests.
func nullEventPublisher(reason, message string) {}
134 changes: 134 additions & 0 deletions pkg/provisioner/ironic/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
"github.com/stretchr/testify/assert"

"github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
"github.com/metal3-io/baremetal-operator/pkg/bmc"
"github.com/metal3-io/baremetal-operator/pkg/provisioner/fixture"
"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients"
Expand Down Expand Up @@ -219,3 +220,136 @@ func TestDeprovision(t *testing.T) {
})
}
}

func TestIronicHasSameImage(t *testing.T) {
nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58"
cases := []struct {
name string
expected bool
node nodes.Node
liveImage bool
hostImage string
hostChecksum string
hostChecksumType v1alpha1.ChecksumType
}{
{
name: "image same",
expected: true,
liveImage: false,
node: nodes.Node{
InstanceInfo: map[string]interface{}{
"image_source": "theimage",
"image_os_hash_value": "thechecksum",
"image_os_hash_algo": "md5",
},
},
hostImage: "theimage",
hostChecksum: "thechecksum",
hostChecksumType: v1alpha1.MD5,
},
{
name: "image different",
expected: false,
liveImage: false,
node: nodes.Node{
InstanceInfo: map[string]interface{}{
"image_source": "theimage",
"image_os_hash_value": "thechecksum",
"image_os_hash_algo": "md5",
},
},
hostImage: "different",
hostChecksum: "thechecksum",
hostChecksumType: v1alpha1.MD5,
},
{
name: "image checksum different",
expected: false,
liveImage: false,
node: nodes.Node{
InstanceInfo: map[string]interface{}{
"image_source": "theimage",
"image_os_hash_value": "thechecksum",
"image_os_hash_algo": "md5",
},
},
hostImage: "theimage",
hostChecksum: "different",
hostChecksumType: v1alpha1.MD5,
},
{
name: "image checksum type different",
expected: false,
liveImage: false,
node: nodes.Node{
InstanceInfo: map[string]interface{}{
"image_source": "theimage",
"image_os_hash_value": "thechecksum",
"image_os_hash_algo": "md5",
},
},
hostImage: "theimage",
hostChecksum: "thechecksum",
hostChecksumType: v1alpha1.SHA512,
},
{
name: "live image same",
liveImage: true,
expected: true,
node: nodes.Node{
InstanceInfo: map[string]interface{}{
"boot_iso": "theimage",
},
},
hostImage: "theimage",
},
{
name: "live image different",
liveImage: true,
expected: false,
node: nodes.Node{
InstanceInfo: map[string]interface{}{
"boot_iso": "theimage",
},
},
hostImage: "different",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ironic := testserver.NewIronic(t).WithDefaultResponses().Node(tc.node)
ironic.Start()
defer ironic.Stop()

inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{
Finished: false,
})
inspector.Start()
defer inspector.Stop()

var host *v1alpha1.BareMetalHost
if tc.liveImage {
host = makeHostLiveIso()
host.Spec.Image.URL = tc.hostImage
} else {
host = makeHost()
host.Spec.Image.URL = tc.hostImage
host.Spec.Image.Checksum = tc.hostChecksum
host.Spec.Image.ChecksumType = tc.hostChecksumType
}
publisher := func(reason, message string) {}
auth := clients.AuthConfig{Type: clients.NoAuth}
prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher,
ironic.Endpoint(), auth, inspector.Endpoint(), auth,
)
if err != nil {
t.Fatalf("could not create provisioner: %s", err)
}

prov.status.ID = nodeUUID
sameImage := prov.ironicHasSameImage(&tc.node)
assert.Equal(t, tc.expected, sameImage)
})
}
}
70 changes: 70 additions & 0 deletions pkg/provisioner/ironic/updateopts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,73 @@ func TestGetUpdateOptsForNodeDell(t *testing.T) {
})
}
}

func TestGetUpdateOptsForNodeLiveIso(t *testing.T) {
eventPublisher := func(reason, message string) {}
auth := clients.AuthConfig{Type: clients.NoAuth}

prov, err := newProvisionerWithSettings(makeHostLiveIso(), bmc.Credentials{}, eventPublisher,
"https://ironic.test", auth, "https://ironic.test", auth,
)
if err != nil {
t.Fatal(err)
}
ironicNode := &nodes.Node{}

patches, err := prov.getUpdateOptsForNode(ironicNode)
if err != nil {
t.Fatal(err)
}

t.Logf("patches: %v", patches)

expected := []struct {
Path string // the node property path
Key string // if value is a map, the key we care about
Value interface{} // the value being passed to ironic (or value associated with the key)
}{
{
Path: "/instance_info/boot_iso",
Value: "not-empty",
},
{
Path: "/deploy_interface",
Value: "ramdisk",
},
{
Path: "/instance_uuid",
Value: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2",
},
{
Path: "/instance_info/root_gb",
Value: 10,
},
{
Path: "/properties/cpu_arch",
Value: "x86_64",
},
{
Path: "/properties/local_gb",
Value: 50,
},
}

for _, e := range expected {
t.Run(e.Path, func(t *testing.T) {
t.Logf("expected: %v", e)
var update nodes.UpdateOperation
for _, patch := range patches {
update = patch.(nodes.UpdateOperation)
if update.Path == e.Path {
break
}
}
if update.Path != e.Path {
t.Errorf("did not find %q in updates", e.Path)
return
}
t.Logf("update: %v", update)
assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path))
})
}
}
Loading

0 comments on commit de55f31

Please sign in to comment.