diff --git a/cmd/kni-install/gather.go b/cmd/kni-install/gather.go index 3b329bc69..555089ea0 100644 --- a/cmd/kni-install/gather.go +++ b/cmd/kni-install/gather.go @@ -15,11 +15,13 @@ import ( "github.com/openshift-metalkube/kni-installer/pkg/terraform" gatheraws "github.com/openshift-metalkube/kni-installer/pkg/terraform/gather/aws" gatherazure "github.com/openshift-metalkube/kni-installer/pkg/terraform/gather/azure" + gatherbaremetal "github.com/openshift-metalkube/kni-installer/pkg/terraform/gather/baremetal" gatherlibvirt "github.com/openshift-metalkube/kni-installer/pkg/terraform/gather/libvirt" gatheropenstack "github.com/openshift-metalkube/kni-installer/pkg/terraform/gather/openstack" "github.com/openshift-metalkube/kni-installer/pkg/types" awstypes "github.com/openshift-metalkube/kni-installer/pkg/types/aws" azuretypes "github.com/openshift-metalkube/kni-installer/pkg/types/azure" + baremetaltypes "github.com/openshift-metalkube/kni-installer/pkg/types/baremetal" libvirttypes "github.com/openshift-metalkube/kni-installer/pkg/types/libvirt" openstacktypes "github.com/openshift-metalkube/kni-installer/pkg/types/openstack" ) @@ -143,6 +145,15 @@ func extractHostAddresses(config *types.InstallConfig, tfstate *terraform.State) if err != nil { logrus.Error(err) } + case baremetaltypes.Name: + bootstrap, err = gatherbaremetal.BootstrapIP(tfstate, config) + if err != nil { + return bootstrap, port, masters, err + } + masters, err = gatherbaremetal.ControlPlaneIPs(tfstate) + if err != nil { + return bootstrap, port, masters, err + } case libvirttypes.Name: bootstrap, err = gatherlibvirt.BootstrapIP(tfstate) if err != nil { diff --git a/data/data/baremetal/main.tf b/data/data/baremetal/main.tf index a0ebc1766..55135f768 100644 --- a/data/data/baremetal/main.tf +++ b/data/data/baremetal/main.tf @@ -4,6 +4,7 @@ provider "libvirt" { provider "ironic" { url = var.ironic_uri + inspector = var.inspector_uri microversion = "1.52" } diff --git a/data/data/baremetal/masters/main.tf b/data/data/baremetal/masters/main.tf index f5e01784b..9e38173b2 100644 --- a/data/data/baremetal/masters/main.tf +++ b/data/data/baremetal/masters/main.tf @@ -32,6 +32,15 @@ resource "ironic_node_v1" "openshift-master-host" { ] } +data "ironic_introspection" "openshift-master-introspection" { + count = var.master_count + + uuid = element( + ironic_node_v1.openshift-master-host.*.id, + count.index, + ) +} + resource "ironic_allocation_v1" "openshift-master-allocation" { name = "master-${count.index}" count = var.master_count diff --git a/data/data/baremetal/variables-baremetal.tf b/data/data/baremetal/variables-baremetal.tf index ee17874ec..207648a31 100644 --- a/data/data/baremetal/variables-baremetal.tf +++ b/data/data/baremetal/variables-baremetal.tf @@ -3,6 +3,11 @@ variable "ironic_uri" { description = "ironic connection URI" } +variable "inspector_uri" { + type = string + description = "ironic inpsector URI" +} + variable "libvirt_uri" { type = string description = "libvirt connection URI" diff --git a/pkg/asset/cluster/tfvars.go b/pkg/asset/cluster/tfvars.go index 75fbb4e93..174fab26b 100644 --- a/pkg/asset/cluster/tfvars.go +++ b/pkg/asset/cluster/tfvars.go @@ -214,6 +214,7 @@ func (t *TerraformVariables) Generate(parents asset.Parents) error { data, err = baremetaltfvars.TFVars( installConfig.Config.Platform.BareMetal.LibvirtURI, installConfig.Config.Platform.BareMetal.IronicURI, + installConfig.Config.Platform.BareMetal.InspectorURI, string(*rhcosImage), "baremetal", "provisioning", diff --git a/pkg/terraform/exec/plugins/Gopkg.lock b/pkg/terraform/exec/plugins/Gopkg.lock index 494964e89..34b72d6b2 100644 --- a/pkg/terraform/exec/plugins/Gopkg.lock +++ b/pkg/terraform/exec/plugins/Gopkg.lock @@ -430,7 +430,7 @@ version = "v1.1.1" [[projects]] - digest = "1:594c8c18e2da9a55a0743cf8f7f9255c26b89a78e50ce4bcf0a81b76d2853916" + digest = "1:c1024fa089f6cf75ea25d2a6d10bb001643e8464d710fa4d8205f8edfa8454bb" name = "github.com/gophercloud/gophercloud" packages = [ ".", @@ -440,6 +440,8 @@ "openstack/baremetal/v1/allocations", "openstack/baremetal/v1/nodes", "openstack/baremetal/v1/ports", + "openstack/baremetalintrospection/noauth", + "openstack/baremetalintrospection/v1/introspection", "openstack/blockstorage/extensions/volumeactions", "openstack/blockstorage/v1/volumes", "openstack/blockstorage/v2/snapshots", @@ -727,12 +729,12 @@ version = "v1.0.0" [[projects]] - digest = "1:61f1cd67d9c6ce439fb943b0f680fa4527d3913b4e895821d592689ffd7b02f3" + digest = "1:a0886267c60262a245d6eeb0be12bf8e165026ad088bce106b9c55c5d613144f" name = "github.com/openshift-metalkube/terraform-provider-ironic" packages = ["ironic"] pruneopts = "NUT" - revision = "e16eaebdfdb17fc6883695b932786eb588625a95" - version = "v0.1.3" + revision = "9f5bba54dad78c10ed572eb2541f8178a5500656" + version = "v0.1.4" [[projects]] branch = "master" @@ -811,12 +813,9 @@ version = "v1.2.1" [[projects]] - digest = "1:0cfe2d873b1ce297b35bc3fdf91916f6a6e116e8d29e7cb8833b4dbfdb66701d" + digest = "1:baa2dc0cf6e02ec0637688ca15f44b2864491bd10be0ac7d34ad35eb85b5925e" name = "github.com/terraform-providers/terraform-provider-null" - packages = [ - ".", - "null", - ] + packages = ["null"] pruneopts = "NUT" revision = "8d3d85a60e207f7cd1ba2f86a69ad5afe18b0e2e" version = "v2.1.2" @@ -1035,7 +1034,6 @@ "github.com/terraform-providers/terraform-provider-azurerm/azurerm", "github.com/terraform-providers/terraform-provider-ignition/ignition", "github.com/terraform-providers/terraform-provider-local/local", - "github.com/terraform-providers/terraform-provider-null", "github.com/terraform-providers/terraform-provider-null/null", "github.com/terraform-providers/terraform-provider-openstack/openstack", "github.com/terraform-providers/terraform-provider-random/random", diff --git a/pkg/terraform/exec/plugins/Gopkg.toml b/pkg/terraform/exec/plugins/Gopkg.toml index 37f7c78eb..2bd0ef273 100644 --- a/pkg/terraform/exec/plugins/Gopkg.toml +++ b/pkg/terraform/exec/plugins/Gopkg.toml @@ -66,7 +66,7 @@ ignored = [ [[constraint]] name = "github.com/openshift-metalkube/terraform-provider-ironic" - version = "v0.1.3" + version = "v0.1.4" [[constraint]] name = "github.com/terraform-providers/terraform-provider-null" diff --git a/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth/doc.go b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth/doc.go new file mode 100644 index 000000000..014180c28 --- /dev/null +++ b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth/doc.go @@ -0,0 +1,15 @@ +/* +Package noauth provides support for noauth bare metal introspection endpoints. + +Example of obtaining and using a client: + + client, err := noauth.NewBareMetalIntrospectionNoAuth(noauth.EndpointOpts{ + IronicInspectorEndpoint: "http://localhost:5050/v1/", + }) + if err != nil { + panic(err) + } + + introspection.GetIntrospectionStatus(client, "a62b8495-52e2-407b-b3cb-62775d04c2b8") +*/ +package noauth diff --git a/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth/requests.go b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth/requests.go new file mode 100644 index 000000000..a1c0857c9 --- /dev/null +++ b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth/requests.go @@ -0,0 +1,36 @@ +package noauth + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" +) + +// EndpointOpts specifies a "noauth" Ironic Inspector Endpoint. +type EndpointOpts struct { + // IronicInspectorEndpoint [required] is currently only used with "noauth" Ironic introspection. + // An Ironic inspector endpoint with "auth_strategy=noauth" is necessary, for example: + // http://ironic.example.com:5050/v1. + IronicInspectorEndpoint string +} + +func initClientOpts(client *gophercloud.ProviderClient, eo EndpointOpts) (*gophercloud.ServiceClient, error) { + sc := new(gophercloud.ServiceClient) + if eo.IronicInspectorEndpoint == "" { + return nil, fmt.Errorf("IronicInspectorEndpoint is required") + } + + sc.Endpoint = gophercloud.NormalizeURL(eo.IronicInspectorEndpoint) + sc.ProviderClient = client + return sc, nil +} + +// NewBareMetalIntrospectionNoAuth creates a ServiceClient that may be used to access a +// "noauth" bare metal introspection service. +func NewBareMetalIntrospectionNoAuth(eo EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(&gophercloud.ProviderClient{}, eo) + + sc.Type = "baremetal-inspector" + + return sc, err +} diff --git a/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/doc.go b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/doc.go new file mode 100644 index 000000000..423823869 --- /dev/null +++ b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/doc.go @@ -0,0 +1,59 @@ +/* +Package introspection contains the functionality for Starting introspection, +Get introspection status, List all introspection statuses, Abort an +introspection, Get stored introspection data and reapply introspection on +stored data. + +API reference https://developer.openstack.org/api-ref/baremetal-introspection/#node-introspection + +Example to Start Introspection + + err := introspection.StartIntrospection(client, NodeUUID, introspection.StartOpts{}).ExtractErr() + if err != nil { + panic(err) + } + +Example to Get an Introspection status + + _, err := introspection.GetIntrospectionStatus(client, NodeUUID).Extract() + if err != nil { + panic(err) + } + +Example to List all introspection statuses + + introspection.ListIntrospections(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + introspectionsList, err := introspection.ExtractIntrospections(page) + if err != nil { + return false, err + } + + for _, n := range introspectionsList { + // Do something + } + + return true, nil + }) + +Example to Abort an Introspection + + err := introspection.AbortIntrospection(client, NodeUUID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Get stored Introspection Data + + v, err := introspection.GetIntrospectionData(c, NodeUUID).Extract() + if err != nil { + panic(err) + } + +Example to apply Introspection Data + + err := introspection.ApplyIntrospectionData(c, NodeUUID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package introspection diff --git a/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/requests.go b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/requests.go new file mode 100644 index 000000000..a2d02ccde --- /dev/null +++ b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/requests.go @@ -0,0 +1,116 @@ +package introspection + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListIntrospectionsOptsBuilder allows extensions to add additional parameters to the +// ListIntrospections request. +type ListIntrospectionsOptsBuilder interface { + ToIntrospectionsListQuery() (string, error) +} + +// ListIntrospectionsOpts allows the filtering and sorting of paginated collections through +// the Introspection API. Filtering is achieved by passing in struct field values that map to +// the node attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListIntrospectionsOpts struct { + // Requests a page size of items. + Limit int `q:"limit"` + + // The ID of the last-seen item. + Marker string `q:"marker"` +} + +// ToIntrospectionsListQuery formats a ListIntrospectionsOpts into a query string. +func (opts ListIntrospectionsOpts) ToIntrospectionsListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListIntrospections makes a request against the Inspector API to list the current introspections. +func ListIntrospections(client *gophercloud.ServiceClient, opts ListIntrospectionsOptsBuilder) pagination.Pager { + url := listIntrospectionsURL(client) + if opts != nil { + query, err := opts.ToIntrospectionsListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + var rpage = IntrospectionPage{pagination.LinkedPageBase{PageResult: r}} + return rpage + }) +} + +// GetIntrospectionStatus makes a request against the Inspector API to get the +// status of a single introspection. +func GetIntrospectionStatus(client *gophercloud.ServiceClient, nodeID string) (r GetIntrospectionStatusResult) { + _, r.Err = client.Get(introspectionURL(client, nodeID), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// StartOptsBuilder allows extensions to add additional parameters to the +// Start request. +type StartOptsBuilder interface { + ToStartIntrospectionQuery() (string, error) +} + +// StartOpts represents options to start an introspection. +type StartOpts struct { + // Whether the current installation of ironic-inspector can manage PXE booting of nodes. + ManageBoot *bool `q:"manage_boot"` +} + +// ToStartIntrospectionQuery converts a StartOpts into a request. +func (opts StartOpts) ToStartIntrospectionQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// StartIntrospection initiate hardware introspection for node NodeID . +// All power management configuration for this node needs to be done prior to calling the endpoint. +func StartIntrospection(client *gophercloud.ServiceClient, nodeID string, opts StartOptsBuilder) (r StartResult) { + _, err := opts.ToStartIntrospectionQuery() + if err != nil { + r.Err = err + return + } + + _, r.Err = client.Post(introspectionURL(client, nodeID), nil, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + + return +} + +// AbortIntrospection abort running introspection. +func AbortIntrospection(client *gophercloud.ServiceClient, nodeID string) (r AbortResult) { + _, r.Err = client.Post(abortIntrospectionURL(client, nodeID), nil, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + + return +} + +// GetIntrospectionData return stored data from successful introspection. +func GetIntrospectionData(client *gophercloud.ServiceClient, nodeID string) (r DataResult) { + _, r.Err = client.Get(introspectionDataURL(client, nodeID), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ReApplyIntrospection triggers introspection on stored unprocessed data. +// No data is allowed to be sent along with the request. +func ReApplyIntrospection(client *gophercloud.ServiceClient, nodeID string) (r ApplyDataResult) { + _, r.Err = client.Post(introspectionUnprocessedDataURL(client, nodeID), nil, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + + return +} diff --git a/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/results.go b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/results.go new file mode 100644 index 000000000..5cb35e999 --- /dev/null +++ b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/results.go @@ -0,0 +1,293 @@ +package introspection + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type introspectionResult struct { + gophercloud.Result +} + +// Extract interprets any introspectionResult as an Introspection, if possible. +func (r introspectionResult) Extract() (*Introspection, error) { + var s Introspection + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractInto will extract a response body into an Introspection struct. +func (r introspectionResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// ExtractIntrospectionsInto will extract a collection of introspectResult pages into a +// slice of Introspection entities. +func ExtractIntrospectionsInto(r pagination.Page, v interface{}) error { + return r.(IntrospectionPage).Result.ExtractIntoSlicePtr(v, "introspection") +} + +// ExtractIntrospections interprets the results of a single page from a +// ListIntrospections() call, producing a slice of Introspection entities. +func ExtractIntrospections(r pagination.Page) ([]Introspection, error) { + var s []Introspection + err := ExtractIntrospectionsInto(r, &s) + return s, err +} + +// IntrospectionPage abstracts the raw results of making a ListIntrospections() +// request against the Inspector API. As OpenStack extensions may freely alter +// the response bodies of structures returned to the client, you may only safely +// access the data provided through the ExtractIntrospections call. +type IntrospectionPage struct { + pagination.LinkedPageBase +} + +// Introspection represents an introspection in the OpenStack Bare Metal Introspector API. +type Introspection struct { + // Whether introspection is finished + Finished bool `json:"finished"` + + // State of the introspection + State string `json:"state"` + + // Error message, can be null; "Canceled by operator" in case introspection was aborted + Error string `json:"error"` + + // UUID of the introspection + UUID string `json:"uuid"` + + // UTC ISO8601 timestamp + StartedAt time.Time `json:"-"` + + // UTC ISO8601 timestamp or null + FinishedAt time.Time `json:"-"` + + // Link to the introspection URL + Links []interface{} `json:"links"` +} + +// IsEmpty returns true if a page contains no Introspection results. +func (r IntrospectionPage) IsEmpty() (bool, error) { + s, err := ExtractIntrospections(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r IntrospectionPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"introspection_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// UnmarshalJSON trie to convert values for started_at and finished_at from the +// json response into RFC3339 standard. Since Introspection API can remove the +// Z from the format, if the conversion fails, it falls back to an RFC3339 +// with no Z format supported by gophercloud. +func (r *Introspection) UnmarshalJSON(b []byte) error { + type tmp Introspection + var s struct { + tmp + StartedAt string `json:"started_at"` + FinishedAt string `json:"finished_at"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Introspection(s.tmp) + + if s.StartedAt != "" { + t, err := time.Parse(time.RFC3339, s.StartedAt) + if err != nil { + t, err = time.Parse(gophercloud.RFC3339NoZ, s.StartedAt) + if err != nil { + return err + } + } + r.StartedAt = t + } + + if s.FinishedAt != "" { + t, err := time.Parse(time.RFC3339, s.FinishedAt) + if err != nil { + t, err = time.Parse(gophercloud.RFC3339NoZ, s.FinishedAt) + if err != nil { + return err + } + } + r.FinishedAt = t + } + + return nil +} + +// GetIntrospectionStatusResult is the response from a GetIntrospectionStatus operation. +// Call its Extract method to interpret it as an Introspection. +type GetIntrospectionStatusResult struct { + introspectionResult +} + +// StartResult is the response from a StartIntrospection operation. +// Call its ExtractErr method to determine if the call succeeded or failed. +type StartResult struct { + gophercloud.ErrResult +} + +// AbortResult is the response from an AbortIntrospection operation. +// Call its ExtractErr method to determine if the call succeeded or failed. +type AbortResult struct { + gophercloud.ErrResult +} + +// Data represents the full introspection data collected. +// The format and contents of the stored data depends on the ramdisk used +// and plugins enabled both in the ramdisk and in inspector itself. +// This structure has been provided for basic compatibility but it +// will need extensions +type Data struct { + AllInterfaces map[string]BaseInterfaceType `json:"all_interfaces"` + BootInterface string `json:"boot_interface"` + CPUArch string `json:"cpu_arch"` + CPUs int `json:"cpus"` + Error string `json:"error"` + Interfaces map[string]BaseInterfaceType `json:"interfaces"` + Inventory InventoryType `json:"inventory"` + IPMIAddress string `json:"ipmi_address"` + LocalGB int `json:"local_gb"` + MACs []string `json:"macs"` + MemoryMB int `json:"memory_mb"` + RootDisk RootDiskType `json:"root_disk"` +} + +// Sub Types defined under Data and deeper in the structure + +type BaseInterfaceType struct { + ClientID string `json:"client_id"` + IP string `json:"ip"` + MAC string `json:"mac"` + PXE bool `json:"pxe"` + LLDPProcessed map[string]interface{} `json:"lldp_processed"` +} + +type BootInfoType struct { + CurrentBootMode string `json:"current_boot_mode"` + PXEInterface string `json:"pxe_interface"` +} + +type CPUType struct { + Architecture string `json:"architecture"` + Count int `json:"count"` + Flags []string `json:"flags"` + Frequency string `json:"frequency"` + ModelName string `json:"model_name"` +} + +type LLDPTLVType struct { + Type int + Value string +} + +type InterfaceType struct { + BIOSDevName string `json:"biosdevname"` + ClientID string `json:"client_id"` + HasCarrier bool `json:"has_carrier"` + IPV4Address string `json:"ipv4_address"` + IPV6Address string `json:"ipv6_address"` + LLDP []LLDPTLVType `json:"lldp"` + MACAddress string `json:"mac_address"` + Name string `json:"name"` + Product string `json:"product"` + Vendor string `json:"vendor"` +} + +type InventoryType struct { + BmcAddress string `json:"bmc_address"` + Boot BootInfoType `json:"boot"` + CPU CPUType `json:"cpu"` + Disks []RootDiskType `json:"disks"` + Interfaces []InterfaceType `json:"interfaces"` + Memory MemoryType `json:"memory"` + SystemVendor SystemVendorType `json:"system_vendor"` +} + +type MemoryType struct { + PhysicalMb int `json:"physical_mb"` + Total int `json:"total"` +} + +type RootDiskType struct { + Hctl string `json:"hctl"` + Model string `json:"model"` + Name string `json:"name"` + Rotational bool `json:"rotational"` + Serial string `json:"serial"` + Size int `json:"size"` + Vendor string `json:"vendor"` + Wwn string `json:"wwn"` + WwnVendorExtension string `json:"wwn_vendor_extension"` + WwnWithExtension string `json:"wwn_with_extension"` +} + +type SystemVendorType struct { + Manufacturer string `json:"manufacturer"` + ProductName string `json:"product_name"` + SerialNumber string `json:"serial_number"` +} + +// UnmarshalJSON interprets an LLDP TLV [key, value] pair as an LLDPTLVType structure +func (r *LLDPTLVType) UnmarshalJSON(data []byte) error { + var list []interface{} + if err := json.Unmarshal(data, &list); err != nil { + return err + } + + if len(list) != 2 { + return fmt.Errorf("Invalid LLDP TLV key-value pair") + } + + fieldtype, ok := list[0].(float64) + if !ok { + return fmt.Errorf("LLDP TLV key is not number") + } + + value, ok := list[1].(string) + if !ok { + return fmt.Errorf("LLDP TLV value is not string") + } + + r.Type = int(fieldtype) + r.Value = value + return nil +} + +// Extract interprets any IntrospectionDataResult as IntrospectionData, if possible. +func (r DataResult) Extract() (*Data, error) { + var s Data + err := r.ExtractInto(&s) + return &s, err +} + +// DataResult represents the response from a GetIntrospectionData operation. +// Call its Extract method to interpret it as a Data. +type DataResult struct { + gophercloud.Result +} + +// ApplyDataResult is the response from an ApplyData operation. +// Call its ExtractErr method to determine if the call succeeded or failed. +type ApplyDataResult struct { + gophercloud.ErrResult +} diff --git a/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/urls.go b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/urls.go new file mode 100644 index 000000000..e48061374 --- /dev/null +++ b/pkg/terraform/exec/plugins/vendor/github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection/urls.go @@ -0,0 +1,23 @@ +package introspection + +import "github.com/gophercloud/gophercloud" + +func listIntrospectionsURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("introspection") +} + +func introspectionURL(client *gophercloud.ServiceClient, nodeID string) string { + return client.ServiceURL("introspection", nodeID) +} + +func abortIntrospectionURL(client *gophercloud.ServiceClient, nodeID string) string { + return client.ServiceURL("introspection", nodeID, "abort") +} + +func introspectionDataURL(client *gophercloud.ServiceClient, nodeID string) string { + return client.ServiceURL("introspection", nodeID, "data") +} + +func introspectionUnprocessedDataURL(client *gophercloud.ServiceClient, nodeID string) string { + return client.ServiceURL("introspection", nodeID, "data", "unprocessed") +} diff --git a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/data_source_ironic_introspection.go b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/data_source_ironic_introspection.go new file mode 100644 index 000000000..16fa5bdfa --- /dev/null +++ b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/data_source_ironic_introspection.go @@ -0,0 +1,115 @@ +package ironic + +import ( + "fmt" + "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection" + "github.com/hashicorp/terraform/helper/schema" + "time" +) + +// Schema resource for an introspection data source, that has some selected details about the node exposed. +func dataSourceIronicIntrospection() *schema.Resource { + return &schema.Resource{ + Read: dataSourceIronicIntrospectionRead, + Schema: map[string]*schema.Schema{ + "uuid": { + Type: schema.TypeString, + Required: true, + }, + "finished": { + Type: schema.TypeBool, + Computed: true, + }, + "error": { + Type: schema.TypeString, + Computed: true, + }, + "started_at": { + Type: schema.TypeString, + Computed: true, + }, + "finished_at": { + Type: schema.TypeString, + Computed: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "interfaces": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + Description: "A list of interfaces that were discovered", + }, + "cpu_arch": { + Type: schema.TypeString, + Computed: true, + Description: "CPU architecture (e.g., x86_64)", + }, + "cpu_count": { + Type: schema.TypeInt, + Computed: true, + Description: "The number of CPU's", + }, + "memory_mb": { + Type: schema.TypeInt, + Computed: true, + Description: "Memory in megabytes", + }, + }, + } +} + +func dataSourceIronicIntrospectionRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(Clients).Inspector + if client == nil { + return fmt.Errorf("inspector endpoint was not defined") + } + + uuid := d.Get("uuid").(string) + + status, err := introspection.GetIntrospectionStatus(client, uuid).Extract() + if err != nil { + return fmt.Errorf("could not get introspection status: %s", err.Error()) + } + + d.Set("finished", status.Finished) + d.Set("finished_at", status.FinishedAt) + d.Set("started_at", status.StartedAt) + d.Set("error", status.Error) + d.Set("state", status.State) + + if status.Finished { + data, err := introspection.GetIntrospectionData(client, uuid).Extract() + if err != nil { + return fmt.Errorf("could not get introspection data: %s", err.Error()) + } + + // Network interface data + var interfaces []map[string]string + for k, v := range data.AllInterfaces { + interfaces = append(interfaces, map[string]string{ + "name": k, + "mac": v.MAC, + "ip": v.IP, + }) + } + d.Set("interfaces", interfaces) + + // CPU data + d.Set("cpu_arch", data.CPUArch) + d.Set("cpu_count", data.CPUs) + + // Memory info + d.Set("memory_mb", data.MemoryMB) + } + + d.SetId(time.Now().UTC().String()) + return nil +} diff --git a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/provider.go b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/provider.go index 87f5917d4..3049aa26c 100644 --- a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/provider.go +++ b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/provider.go @@ -2,11 +2,20 @@ package ironic import ( "fmt" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/baremetal/noauth" + noauthintrospection "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" + "log" ) +// Clients stores the client connection information for Ironic and Inspector +type Clients struct { + Ironic *gophercloud.ServiceClient + Inspector *gophercloud.ServiceClient +} + // Provider Ironic func Provider() terraform.ResourceProvider { return &schema.Provider{ @@ -17,6 +26,12 @@ func Provider() terraform.ResourceProvider { DefaultFunc: schema.EnvDefaultFunc("IRONIC_ENDPOINT", ""), Description: descriptions["url"], }, + "inspector": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("IRONIC_INSPECTOR_ENDPOINT", ""), + Description: descriptions["inspector"], + }, "microversion": { Type: schema.TypeString, Required: true, @@ -30,6 +45,9 @@ func Provider() terraform.ResourceProvider { "ironic_allocation_v1": resourceAllocationV1(), "ironic_deployment": resourceDeployment(), }, + DataSourcesMap: map[string]*schema.Resource{ + "ironic_introspection": dataSourceIronicIntrospection(), + }, ConfigureFunc: configureProvider, } } @@ -39,25 +57,41 @@ var descriptions map[string]string func init() { descriptions = map[string]string{ "url": "The authentication endpoint for Ironic", + "inspector": "The endpoint for Ironic inspector", "microversion": "The microversion to use for Ironic", } } // Creates a noauth Ironic client func configureProvider(schema *schema.ResourceData) (interface{}, error) { + var clients Clients + url := schema.Get("url").(string) if url == "" { return nil, fmt.Errorf("url is required for ironic provider") } + log.Printf("[DEBUG] Ironic endpoint is %s", url) - client, err := noauth.NewBareMetalNoAuth(noauth.EndpointOpts{ + ironic, err := noauth.NewBareMetalNoAuth(noauth.EndpointOpts{ IronicEndpoint: url, }) if err != nil { return nil, err } + ironic.Microversion = schema.Get("microversion").(string) + clients.Ironic = ironic - client.Microversion = schema.Get("microversion").(string) + inspectorURL := schema.Get("inspector").(string) + if inspectorURL != "" { + log.Printf("[DEBUG] Inspector endpoint is %s", inspectorURL) + inspector, err := noauthintrospection.NewBareMetalIntrospectionNoAuth(noauthintrospection.EndpointOpts{ + IronicInspectorEndpoint: inspectorURL, + }) + if err != nil { + return nil, fmt.Errorf("could not configure inspector endpoint: %s", err.Error()) + } + clients.Inspector = inspector + } - return client, err + return clients, err } diff --git a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_allocation_v1.go b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_allocation_v1.go index d1cb4c2b9..b4c7199cf 100644 --- a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_allocation_v1.go +++ b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_allocation_v1.go @@ -68,7 +68,7 @@ func resourceAllocationV1() *schema.Resource { // Create an allocation, including driving Ironic's state machine func resourceAllocationV1Create(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic result, err := allocations.Create(client, allocationSchemaToCreateOpts(d)).Extract() if err != nil { @@ -110,7 +110,7 @@ func resourceAllocationV1Create(d *schema.ResourceData, meta interface{}) error // Read the allocation's data from Ironic func resourceAllocationV1Read(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic result, err := allocations.Get(client, d.Id()).Extract() if err != nil { @@ -131,7 +131,7 @@ func resourceAllocationV1Read(d *schema.ResourceData, meta interface{}) error { // Delete an allocation from Ironic if it exists func resourceAllocationV1Delete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic _, err := allocations.Get(client, d.Id()).Extract() if _, ok := err.(gophercloud.ErrDefault404); ok { diff --git a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_deployment.go b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_deployment.go index 26473ac12..e5f4f87b0 100644 --- a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_deployment.go +++ b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_deployment.go @@ -2,7 +2,6 @@ package ironic import ( "fmt" - "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" utils "github.com/gophercloud/utils/openstack/baremetal/v1/nodes" "github.com/hashicorp/terraform/helper/schema" @@ -60,7 +59,7 @@ func resourceDeployment() *schema.Resource { // Create an deployment, including driving Ironic's state machine func resourceDeploymentCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic // Reload the resource before returning defer resourceDeploymentRead(d, meta) @@ -95,7 +94,7 @@ func resourceDeploymentCreate(d *schema.ResourceData, meta interface{}) error { // Read the deployment's data from Ironic func resourceDeploymentRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic // Ensure node exists first id := d.Get("node_uuid").(string) @@ -112,6 +111,6 @@ func resourceDeploymentRead(d *schema.ResourceData, meta interface{}) error { // Delete an deployment from Ironic - this cleans the node and returns it's state to 'available' func resourceDeploymentDelete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic return ChangeProvisionStateToTarget(client, d.Id(), "deleted", nil) } diff --git a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_node_v1.go b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_node_v1.go index 8af15c75d..1c94f4e85 100644 --- a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_node_v1.go +++ b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_node_v1.go @@ -180,7 +180,7 @@ func resourceNodeV1() *schema.Resource { // Create a node, including driving Ironic's state machine func resourceNodeV1Create(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic // Create the node object in Ironic createOpts := schemaToCreateOpts(d) @@ -266,7 +266,7 @@ func resourceNodeV1Create(d *schema.ResourceData, meta interface{}) error { // Read the node's data from Ironic func resourceNodeV1Read(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic node, err := nodes.Get(client, d.Id()).Extract() if err != nil { @@ -306,7 +306,7 @@ func resourceNodeV1Read(d *schema.ResourceData, meta interface{}) error { // Update a node's state based on the terraform config - TODO: handle everything func resourceNodeV1Update(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic d.Partial(true) @@ -403,7 +403,7 @@ func resourceNodeV1Update(d *schema.ResourceData, meta interface{}) error { // Delete a node from Ironic func resourceNodeV1Delete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic if err := ChangeProvisionStateToTarget(client, d.Id(), "deleted", nil); err != nil { return err } diff --git a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_port_v1.go b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_port_v1.go index 1564c3edb..df5d42b0e 100644 --- a/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_port_v1.go +++ b/pkg/terraform/exec/plugins/vendor/github.com/openshift-metalkube/terraform-provider-ironic/ironic/resource_ironic_port_v1.go @@ -1,7 +1,6 @@ package ironic import ( - "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/baremetal/v1/ports" "github.com/hashicorp/terraform/helper/schema" ) @@ -51,7 +50,7 @@ func resourcePortV1() *schema.Resource { } func resourcePortV1Create(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic opts := portSchemaToCreateOpts(d) result, err := ports.Create(client, opts).Extract() if err != nil { @@ -63,7 +62,7 @@ func resourcePortV1Create(d *schema.ResourceData, meta interface{}) error { } func resourcePortV1Read(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gophercloud.ServiceClient) + client := meta.(Clients).Ironic port, err := ports.Get(client, d.Id()).Extract() if err != nil { return err diff --git a/pkg/terraform/exec/plugins/vendor/github.com/terraform-providers/terraform-provider-null/main.go b/pkg/terraform/exec/plugins/vendor/github.com/terraform-providers/terraform-provider-null/main.go deleted file mode 100644 index 05cb3a6dc..000000000 --- a/pkg/terraform/exec/plugins/vendor/github.com/terraform-providers/terraform-provider-null/main.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "github.com/hashicorp/terraform/plugin" - "github.com/terraform-providers/terraform-provider-null/null" -) - -func main() { - plugin.Serve(&plugin.ServeOpts{ - ProviderFunc: null.Provider}) -} diff --git a/pkg/terraform/gather/baremetal/ip.go b/pkg/terraform/gather/baremetal/ip.go new file mode 100644 index 000000000..c3798cac7 --- /dev/null +++ b/pkg/terraform/gather/baremetal/ip.go @@ -0,0 +1,108 @@ +// Package baremetal contains utilities that help gather Baremetal specific +// information from terraform state. +package baremetal + +import ( + "fmt" + "github.com/libvirt/libvirt-go" + "github.com/openshift-metalkube/kni-installer/pkg/types" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "strings" + + "github.com/openshift-metalkube/kni-installer/pkg/terraform" +) + +// BootstrapIP returns the ip address for bootstrap host. Baremetal relies on a libvirt bootstrap node, that gets it's +// IP from DHCP. +func BootstrapIP(tfs *terraform.State, config *types.InstallConfig) (string, error) { + br, err := terraform.LookupResource(tfs, "module.bootstrap", "libvirt_domain", "bootstrap") + if err != nil { + return "", errors.Wrap(err, "failed to lookup bootstrap libvirt domain") + } + + if len(br.Instances) == 0 { + return "", errors.New("no bootstrap instance found") + } + + mac, err := getMACFromInstance(&br.Instances[0], config.BareMetal.ExternalBridge) + if err != nil { + return "", errors.Wrap(err, "could not fetch bootstrap mac address") + } + + ip, err := getLeaseForMac(config.BareMetal.LibvirtURI, config.BareMetal.ExternalBridge, mac) + if err != nil { + return "", errors.Wrap(err, "no bootstrap dhcp lease found") + } + + return ip, nil +} + +// ControlPlaneIPs returns the ip addresses for control plane hosts, retrieved via Ironic introspection. +func ControlPlaneIPs(tfs *terraform.State) ([]string, error) { + mrs, err := terraform.LookupResource(tfs, "module.masters", "ironic_introspection", "openshift-master-introspection") + if err != nil { + return nil, errors.Wrap(err, "failed to lookup masters introspection data") + } + + var errs []error + var masters []string + for idx, inst := range mrs.Instances { + interfaces, _, err := unstructured.NestedSlice(inst.Attributes, "interfaces") + if err != nil { + errs = append(errs, errors.Wrapf(err, "could not get interfaces for master-%d", idx)) + } + ip, _, err := unstructured.NestedString(interfaces[0].(map[string]interface{}), "ip") + masters = append(masters, ip) + } + return masters, utilerrors.NewAggregate(errs) +} + +// getMACFromInstance finds the MAC address from the terraform state data +func getMACFromInstance(inst *terraform.StateResourceInstance, network string) (string, error) { + interfaces, _, err := unstructured.NestedSlice(inst.Attributes, "network_interface") + if err != nil { + return "", err + } + + for _, iface := range interfaces { + bridge, _, err := unstructured.NestedString(iface.(map[string]interface{}), "bridge") + if err != nil { + return "", err + } + if bridge == network { + mac, _, err := unstructured.NestedString(iface.(map[string]interface{}), "mac") + return mac, err + } + } + + return "", fmt.Errorf("could not find mac") +} + +// getLeaseForMac finds the DHCP lease for the given MAC address on a particular bridge. +func getLeaseForMac(libvirtURI, bridge, mac string) (string, error) { + conn, err := libvirt.NewConnect(libvirtURI) + if err != nil { + return "", errors.Wrap(err, "could not connect to libvirt") + } + defer conn.Close() + + network, err := conn.LookupNetworkByName(bridge) + if err != nil { + return "", errors.Wrap(err, "could not find libvirt network") + } + + leases, err := network.GetDHCPLeases() + if err != nil { + return "", errors.Wrap(err, "could not fetch dhcp leases") + } + + for _, lease := range leases { + if strings.ToUpper(lease.Mac) == strings.ToUpper(mac) { + return lease.IPaddr, nil + } + } + + return "", fmt.Errorf("could not fetch dhcp lease") +} diff --git a/pkg/tfvars/baremetal/baremetal.go b/pkg/tfvars/baremetal/baremetal.go index 14457d60d..5753b831a 100644 --- a/pkg/tfvars/baremetal/baremetal.go +++ b/pkg/tfvars/baremetal/baremetal.go @@ -13,6 +13,7 @@ import ( type config struct { LibvirtURI string `json:"libvirt_uri,omitempty"` IronicURI string `json:"ironic_uri,omitempty"` + InspectorURI string `json:"inspector_uri,omitempty"` Image string `json:"os_image,omitempty"` ExternalBridge string `json:"external_bridge,omitempty"` ProvisioningBridge string `json:"provisioning_bridge,omitempty"` @@ -26,7 +27,7 @@ type config struct { } // TFVars generates bare metal specific Terraform variables. -func TFVars(libvirtURI, ironicURI, osImage, externalBridge, provisioningBridge string, platformHosts []*baremetal.Host, image baremetal.Image) ([]byte, error) { +func TFVars(libvirtURI, ironicURI, inspectorURI, osImage, externalBridge, provisioningBridge string, platformHosts []*baremetal.Host, image baremetal.Image) ([]byte, error) { osImage, err := libvirttfvars.CachedImage(osImage) if err != nil { return nil, errors.Wrap(err, "failed to use cached libvirt image") @@ -93,6 +94,7 @@ func TFVars(libvirtURI, ironicURI, osImage, externalBridge, provisioningBridge s cfg := &config{ LibvirtURI: libvirtURI, IronicURI: ironicURI, + InspectorURI: inspectorURI, Image: osImage, ExternalBridge: externalBridge, ProvisioningBridge: provisioningBridge, diff --git a/pkg/types/baremetal/defaults/platform.go b/pkg/types/baremetal/defaults/platform.go index ab98849bd..90bd949b0 100644 --- a/pkg/types/baremetal/defaults/platform.go +++ b/pkg/types/baremetal/defaults/platform.go @@ -11,10 +11,11 @@ import ( const ( LibvirtURI = "qemu:///system" IronicURI = "http://localhost:6385/v1" + InspectorURI = "http://localhost:5050/v1" ExternalBridge = "baremetal" ProvisioningBridge = "provisioning" HardwareProfile = "default" - ApiVIP = "" + ApiVIP = "" ) // SetPlatformDefaults sets the defaults for the platform. @@ -27,6 +28,10 @@ func SetPlatformDefaults(p *baremetal.Platform, c *types.InstallConfig) { p.IronicURI = IronicURI } + if p.InspectorURI == "" { + p.InspectorURI = InspectorURI + } + if p.ExternalBridge == "" { p.ExternalBridge = ExternalBridge } diff --git a/pkg/types/baremetal/platform.go b/pkg/types/baremetal/platform.go index 746e22b3b..fa9d52ce0 100644 --- a/pkg/types/baremetal/platform.go +++ b/pkg/types/baremetal/platform.go @@ -34,6 +34,11 @@ type Platform struct { // +optional IronicURI string `json:"ironic_uri,omitempty"` + // InspectorURI is the identifier for the Inspector connection. It must be + // reachable from the host where the installer is run. + // +optional + InspectorURI string `json:"inspector_uri,omitempty"` + // External bridge is used for external communication. // +optional ExternalBridge string `json:"external_bridge,omitempty"`