diff --git a/pkg/controller/baremetalhost/baremetalhost_controller.go b/pkg/controller/baremetalhost/baremetalhost_controller.go index 382bdae915..7b15b4517b 100644 --- a/pkg/controller/baremetalhost/baremetalhost_controller.go +++ b/pkg/controller/baremetalhost/baremetalhost_controller.go @@ -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" @@ -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") @@ -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, @@ -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 } @@ -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, diff --git a/pkg/provisioner/ironic/client/client.go b/pkg/provisioner/ironic/client/client.go new file mode 100644 index 0000000000..618e8b16ed --- /dev/null +++ b/pkg/provisioner/ironic/client/client.go @@ -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 +} diff --git a/pkg/provisioner/ironic/discovery/discovery.go b/pkg/provisioner/ironic/discovery/discovery.go new file mode 100644 index 0000000000..1a448b6a33 --- /dev/null +++ b/pkg/provisioner/ironic/discovery/discovery.go @@ -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 + }) +} diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go index fab9139a04..ca9b09d61d 100644 --- a/pkg/provisioner/ironic/ironic.go +++ b/pkg/provisioner/ironic/ironic.go @@ -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" @@ -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" ) @@ -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 @@ -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 @@ -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, ) @@ -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), @@ -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 {