Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions pkg/controller/baremetalhost/baremetalhost_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/metal3-io/baremetal-operator/pkg/provisioner/demo"
"github.com/metal3-io/baremetal-operator/pkg/provisioner/fixture"
"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic"
"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/discovery"
"github.com/metal3-io/baremetal-operator/pkg/utils"

"github.com/go-logr/logr"
Expand Down Expand Up @@ -81,9 +82,12 @@ func Add(mgr manager.Manager) error {
return add(mgr, newReconciler(mgr))
}

// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
// newReconciler returns a new reconciler
func newReconciler(mgr manager.Manager) *ReconcileBareMetalHost {
var provisionerFactory provisioner.Factory
var discoveryScanner manager.Runnable
var err error

switch {
case runInTestMode:
log.Info("USING TEST MODE")
Expand All @@ -93,17 +97,23 @@ func newReconciler(mgr manager.Manager) reconcile.Reconciler {
provisionerFactory = demo.New
default:
provisionerFactory = ironic.New
discoveryScanner, err = discovery.Scanner(mgr, time.Second*10)
if err != nil {
log.Error(err, "failed to start discovery scanner")
discoveryScanner = nil
}
ironic.LogStartup()
}
return &ReconcileBareMetalHost{
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
provisionerFactory: provisionerFactory,
discoveryScanner: discoveryScanner,
}
}

// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r reconcile.Reconciler) error {
func add(mgr manager.Manager, r *ReconcileBareMetalHost) error {
// Create a new controller
c, err := controller.New("metal3-baremetalhost-controller", mgr,
controller.Options{MaxConcurrentReconciles: maxConcurrentReconciles,
Expand All @@ -126,6 +136,15 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error {
IsController: true,
OwnerType: &metal3v1alpha1.BareMetalHost{},
})
if err != nil {
return err
}

// Start the discovery manager
if r.discoveryScanner != nil {
err = mgr.Add(r.discoveryScanner)
}

return err
}

Expand All @@ -138,6 +157,7 @@ type ReconcileBareMetalHost struct {
client client.Client
scheme *runtime.Scheme
provisionerFactory provisioner.Factory
discoveryScanner manager.Runnable
}

// Instead of passing a zillion arguments to the action of a phase,
Expand Down
55 changes: 55 additions & 0 deletions pkg/provisioner/ironic/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package client

import (
"fmt"
"os"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/baremetal/noauth"
noauthintrospection "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth"
)

// IronicEndpoint is the location of the Ironic API service.
var IronicEndpoint string

// InspectorEndpoint is the location of the Ironic Inspector API.
var InspectorEndpoint string

func init() {
IronicEndpoint = os.Getenv("IRONIC_ENDPOINT")
if IronicEndpoint == "" {
fmt.Fprintf(os.Stderr, "Cannot start: No IRONIC_ENDPOINT variable set\n")
os.Exit(1)
}
InspectorEndpoint = os.Getenv("IRONIC_INSPECTOR_ENDPOINT")
if InspectorEndpoint == "" {
fmt.Fprintf(os.Stderr, "Cannot start: No IRONIC_INSPECTOR_ENDPOINT variable set")
os.Exit(1)
}
}

// New creates a new ironic client
func New() (client *gophercloud.ServiceClient, err error) {
client, err = noauth.NewBareMetalNoAuth(noauth.EndpointOpts{
IronicEndpoint: IronicEndpoint,
})
if err != nil {
return nil, err
}
// Ensure we have a microversion high enough to get the features
// we need.
client.Microversion = "1.56"
return client, nil
}

// NewInspector creates a new ironic-inspecctor client
func NewInspector() (client *gophercloud.ServiceClient, err error) {
client, err = noauthintrospection.NewBareMetalIntrospectionNoAuth(
noauthintrospection.EndpointOpts{
IronicInspectorEndpoint: InspectorEndpoint,
})
if err != nil {
return nil, err
}
return client, nil
}
146 changes: 146 additions & 0 deletions pkg/provisioner/ironic/discovery/discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package discovery

import (
"context"
"time"

"github.com/pkg/errors"
logf "sigs.k8s.io/controller-runtime/pkg/log"

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
"github.com/gophercloud/gophercloud/openstack/baremetal/v1/ports"
"github.com/gophercloud/gophercloud/pagination"

metal3v1alpha1 "github.com/metal3-io/baremetal-operator/pkg/apis/metal3/v1alpha1"
ironicclient "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/client"
)

var log = logf.Log.WithName("baremetalhost-discovery")

// Scanner returns a new manager for identifying hosts that have been
// seen by ironic but do not have a matching BareMetalHost resource.
func Scanner(mgr manager.Manager, period time.Duration) (scanner manager.Runnable, err error) {
ironic, err := ironicclient.New()
if err != nil {
return nil, errors.Wrap(err, "failed to create ironic client")
}
inspector, err := ironicclient.NewInspector()
if err != nil {
return nil, errors.Wrap(err, "failed to create ironic-inspector client")
}
scanner = &discoveryScanner{
client: mgr.GetClient(),
period: period,
ironic: ironic,
inspector: inspector,
}
return scanner, nil
}

type discoveryScanner struct {
// kubernetes API client
client client.Client
// scanning interval (number of seconds)
period time.Duration
// a client for talking to ironic
ironic *gophercloud.ServiceClient
// a client for talking to ironic-inspector
inspector *gophercloud.ServiceClient
}

func (scanner *discoveryScanner) Start(done <-chan struct{}) error {
for {
select {
case <-done:
return nil
case <-time.After(scanner.period):
scanner.poll()
}
}
}

func (scanner *discoveryScanner) poll() {

// Build a list of all of the known BareMetalHost resources so we
// can match them to information Ironic gives us.
ctx := context.TODO()
hostList := metal3v1alpha1.BareMetalHostList{}
err := scanner.client.List(ctx, &hostList)
if err != nil {
log.Error(err, "failed to fetch list of hosts")
return
}

// Organize the data to make it easier to find existing hosts
// based on data Ironic will have.
byUUID := make(map[string]metal3v1alpha1.BareMetalHost)
byName := make(map[string]metal3v1alpha1.BareMetalHost)
byMAC := make(map[string]metal3v1alpha1.BareMetalHost)
for _, host := range hostList.Items {
byName[host.Name] = host
if host.Status.Provisioning.ID != "" {
byUUID[host.Status.Provisioning.ID] = host
}
if host.Spec.BootMACAddress != "" {
byMAC[host.Spec.BootMACAddress] = host
}
}

// Build a map connecting the UUID of the nodes in ironic to the
// Port MAC address in ironic so we can easily find the MAC for
// any hosts we have to create.
uuidToMAC := make(map[string]string)
portPages := ports.ListDetail(scanner.ironic, ports.ListOpts{})
portPages.EachPage(func(p pagination.Page) (bool, error) {
portList, err := ports.ExtractPorts(p)
if err != nil {
return false, err
}
for _, port := range portList {
uuidToMAC[port.NodeUUID] = port.Address
}
return true, nil
})

// Look through the nodes that ironic knows and create
// BareMetalHost resources for any that do not exist.
//
// FIXME: Should we constrain this query at all? Maybe only look
// for hosts that are in a particular state?
nodePages := nodes.ListDetail(scanner.ironic, nodes.ListOpts{})
nodePages.EachPage(func(p pagination.Page) (bool, error) {
nodeList, err := nodes.ExtractNodes(p)
if err != nil {
return false, err
}
for _, node := range nodeList {
var ok bool
_, ok = byUUID[node.UUID]
if ok {
log.Info("host is known by uuid", "uuid", node.UUID, "name", node.Name)
continue
}
_, ok = byName[node.Name]
if ok {
log.Info("host is known by name", "uuid", node.UUID, "name", node.Name)
continue
}
mac, ok := uuidToMAC[node.UUID]
if !ok {
log.Info("no MAC found for host in ironic", "uuid", node.UUID, "name", node.Name)
continue
}
_, ok = byMAC[mac]
if ok {
log.Info("host is known by mac", "uuid", node.UUID, "name", node.Name, "mac", mac)
continue
}
log.Info("found new host", "mac", mac, "uuid", node.UUID, "name", node.Name)
}
return true, nil
})
}
41 changes: 10 additions & 31 deletions pkg/provisioner/ironic/ironic.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import (
"sigs.k8s.io/yaml"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/baremetal/noauth"
"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
"github.com/gophercloud/gophercloud/openstack/baremetal/v1/ports"
noauthintrospection "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth"
"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"

"github.com/pkg/errors"
Expand All @@ -24,6 +22,7 @@ import (
"github.com/metal3-io/baremetal-operator/pkg/bmc"
"github.com/metal3-io/baremetal-operator/pkg/hardware"
"github.com/metal3-io/baremetal-operator/pkg/provisioner"
ironicclient "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/client"
"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/devicehints"
"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/hardwaredetails"
)
Expand All @@ -35,8 +34,6 @@ var powerRequeueDelay = time.Second * 10
var introspectionRequeueDelay = time.Second * 15
var deployKernelURL string
var deployRamdiskURL string
var ironicEndpoint string
var inspectorEndpoint string

const (
// See nodes.Node.PowerState for details
Expand All @@ -58,16 +55,6 @@ func init() {
fmt.Fprintf(os.Stderr, "Cannot start: No DEPLOY_RAMDISK_URL variable set\n")
os.Exit(1)
}
ironicEndpoint = os.Getenv("IRONIC_ENDPOINT")
if ironicEndpoint == "" {
fmt.Fprintf(os.Stderr, "Cannot start: No IRONIC_ENDPOINT variable set\n")
os.Exit(1)
}
inspectorEndpoint = os.Getenv("IRONIC_INSPECTOR_ENDPOINT")
if inspectorEndpoint == "" {
fmt.Fprintf(os.Stderr, "Cannot start: No IRONIC_INSPECTOR_ENDPOINT variable set")
os.Exit(1)
}
}

// Provisioner implements the provisioning.Provisioner interface
Expand Down Expand Up @@ -95,8 +82,8 @@ type ironicProvisioner struct {
// emit once on startup but that is interal to this package.
func LogStartup() {
log.Info("ironic settings",
"endpoint", ironicEndpoint,
"inspectorEndpoint", inspectorEndpoint,
"endpoint", ironicclient.IronicEndpoint,
"inspectorEndpoint", ironicclient.InspectorEndpoint,
"deployKernelURL", deployKernelURL,
"deployRamdiskURL", deployRamdiskURL,
)
Expand All @@ -105,26 +92,18 @@ func LogStartup() {
// A private function to construct an ironicProvisioner (rather than a
// Provisioner interface) in a consistent way for tests.
func newProvisioner(host *metal3v1alpha1.BareMetalHost, bmcCreds bmc.Credentials, publisher provisioner.EventPublisher) (*ironicProvisioner, error) {
client, err := noauth.NewBareMetalNoAuth(noauth.EndpointOpts{
IronicEndpoint: ironicEndpoint,
})
client, err := ironicclient.New()
if err != nil {
return nil, err
return nil, errors.Wrap(err, "failed to create ironic client")
}
bmcAccess, err := bmc.NewAccessDetails(host.Spec.BMC.Address, host.Spec.BMC.DisableCertificateVerification)
inspector, err := ironicclient.NewInspector()
if err != nil {
return nil, errors.Wrap(err, "failed to parse BMC address information")
return nil, errors.Wrap(err, "failed to create ironic-inspector client")
}
inspector, err := noauthintrospection.NewBareMetalIntrospectionNoAuth(
noauthintrospection.EndpointOpts{
IronicInspectorEndpoint: inspectorEndpoint,
})
bmcAccess, err := bmc.NewAccessDetails(host.Spec.BMC.Address, host.Spec.BMC.DisableCertificateVerification)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "failed to parse BMC address information")
}
// Ensure we have a microversion high enough to get the features
// we need.
client.Microversion = "1.56"
p := &ironicProvisioner{
host: host,
status: &(host.Status.Provisioning),
Expand Down Expand Up @@ -790,7 +769,7 @@ func (p *ironicProvisioner) Adopt() (result provisioner.Result, err error) {
var ironicNode *nodes.Node

if ironicNode, err = p.findExistingHost(); err != nil {
err = errors.Wrap(err, "could not find host to adpot")
err = errors.Wrap(err, "could not find host to adopt")
return
}
if ironicNode == nil {
Expand Down