diff --git a/Makefile b/Makefile index 5f8808521..e7cb787ce 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ OPERATOR_INDEX_IMAGE ?= $(REGISTRY)/$(REGISTRY_ACCOUNT)/forklift-operator-index: POPULATOR_CONTROLLER_IMAGE ?= $(REGISTRY)/$(REGISTRY_ACCOUNT)/populator-controller:$(REGISTRY_TAG) OVIRT_POPULATOR_IMAGE ?= $(REGISTRY)/$(REGISTRY_ACCOUNT)/ovirt-populator:$(REGISTRY_TAG) OPENSTACK_POPULATOR_IMAGE ?= $(REGISTRY)/$(REGISTRY_ACCOUNT)/openstack-populator:$(REGISTRY_TAG) +OVA_PARSER_IMAGE ?= $(REGISTRY)/$(REGISTRY_ACCOUNT)/ova-parser:$(REGISTRY_TAG) ### External images MUST_GATHER_IMAGE ?= quay.io/kubev2v/forklift-must-gather:latest @@ -210,7 +211,8 @@ build-operator-bundle-image: check_container_runtmime --action_env API_IMAGE=$(API_IMAGE) \ --action_env POPULATOR_CONTROLLER_IMAGE=$(POPULATOR_CONTROLLER_IMAGE) \ --action_env OVIRT_POPULATOR_IMAGE=$(OVIRT_POPULATOR_IMAGE) \ - --action_env OPENSTACK_POPULATOR_IMAGE=$(OPENSTACK_POPULATOR_IMAGE) + --action_env OPENSTACK_POPULATOR_IMAGE=$(OPENSTACK_POPULATOR_IMAGE)\ + --action_env OVA_PARSER_IMAGE=$(OVA_PARSER_IMAGE) push-operator-bundle-image: build-operator-bundle-image $(CONTAINER_CMD) tag bazel/operator:forklift-operator-bundle-image $(OPERATOR_BUNDLE_IMAGE) @@ -257,6 +259,12 @@ build-openstack-populator-image: check_container_runtmime push-openstack-populator-image: build-openstack-populator-image $(CONTAINER_CMD) push $(OPENSTACK_POPULATOR_IMAGE) +build-ova-parses-image: build-ova-parses-image + $(CONTAINER_CMD) build -f hack/ova-parser/Containerfile -t $(OVA_PARSER_IMAGE) . + +push-ova-parses-image: push-ova-parses-image + $(CONTAINER_CMD) push $(OVA_PARSER_IMAGE) + build-all-images: build-api-image \ build-controller-image \ build-validation-image \ @@ -267,7 +275,8 @@ build-all-images: build-api-image \ build-operator-index-image \ build-populator-controller-image \ build-ovirt-populator-image \ - build-openstack-populator-image + build-openstack-populator-image\ + build-ova-parses-image push-all-images: push-api-image \ push-controller-image \ @@ -279,7 +288,8 @@ push-all-images: push-api-image \ push-operator-index-image \ push-populator-controller-image \ push-ovirt-populator-image \ - push-openstack-populator-image + push-openstack-populator-image\ + push-ova-parses-image .PHONY: check_container_runtmime check_container_runtmime: diff --git a/cmd/ova-parser/BUILD.bazel b/cmd/ova-parser/BUILD.bazel new file mode 100644 index 000000000..37f7e4560 --- /dev/null +++ b/cmd/ova-parser/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "ova-parser_lib", + srcs = ["ova-parser.go"], + importpath = "github.com/konveyor/forklift-controller/cmd/ova-parser", + visibility = ["//visibility:private"], +) + +go_binary( + name = "ova-parser", + embed = [":ova-parser_lib"], + visibility = ["//visibility:public"], +) diff --git a/cmd/ova-parser/ova-parser.go b/cmd/ova-parser/ova-parser.go new file mode 100644 index 000000000..ff8510476 --- /dev/null +++ b/cmd/ova-parser/ova-parser.go @@ -0,0 +1,450 @@ +package main + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" +) + +// xml struct +type Item struct { + AllocationUnits string `xml:"AllocationUnits,omitempty"` + Description string `xml:"Description,omitempty"` + ElementName string `xml:"ElementName"` + InstanceID string `xml:"InstanceID"` + ResourceType string `xml:"ResourceType"` + VirtualQuantity int32 `xml:"VirtualQuantity"` + Address string `xml:"Address,omitempty"` + ResourceSubType string `xml:"ResourceSubType,omitempty"` + Parent string `xml:"Parent,omitempty"` + HostResource string `xml:"HostResource,omitempty"` + Connection string `xml:"Connection,omitempty"` + Configs []VirtualConfig `xml:"Config"` +} + +type VirtualConfig struct { + XMLName xml.Name `xml:"http://www.vmware.com/schema/ovf Config"` + Required string `xml:"required,attr"` + Key string `xml:"key,attr"` + Value string `xml:"value,attr"` +} + +type VirtualHardwareSection struct { + Info string `xml:"Info"` + Items []Item `xml:"Item"` + Configs []VirtualConfig `xml:"Config"` +} + +type DiskSection struct { + XMLName xml.Name `xml:"DiskSection"` + Info string `xml:"Info"` + Disks []Disk `xml:"Disk"` +} + +type Disk struct { + XMLName xml.Name `xml:"Disk"` + Capacity string `xml:"capacity,attr"` + CapacityAllocationUnits string `xml:"capacityAllocationUnits,attr"` + DiskId string `xml:"diskId,attr"` + FileRef string `xml:"fileRef,attr"` + Format string `xml:"format,attr"` + PopulatedSize string `xml:"populatedSize,attr"` +} + +type NetworkSection struct { + XMLName xml.Name `xml:"NetworkSection"` + Info string `xml:"Info"` + Networks []Network `xml:"Network"` +} + +type Network struct { + XMLName xml.Name `xml:"Network"` + Name string `xml:"name,attr"` + Description string `xml:"Description"` +} + +type VirtualSystem struct { + ID string `xml:"id,attr"` + Name string `xml:"Name"` + OperatingSystemSection struct { + Info string `xml:"Info"` + Description string `xml:"Description"` + } `xml:"OperatingSystemSection"` + HardwareSection VirtualHardwareSection `xml:"VirtualHardwareSection"` +} + +type Envelope struct { + XMLName xml.Name `xml:"Envelope"` + VirtualSystem []VirtualSystem `xml:"VirtualSystem"` + DiskSection DiskSection `xml:"DiskSection"` + NetworkSection NetworkSection `xml:"NetworkSection"` +} + +// vm struct +type VM struct { + Name string + OvaPath string + RevisionValidated int64 + PolicyVersion int + UUID string + Firmware string + CpuAffinity []int32 + CpuHotAddEnabled bool + CpuHotRemoveEnabled bool + MemoryHotAddEnabled bool + FaultToleranceEnabled bool + CpuCount int32 + CoresPerSocket int32 + MemoryMB int32 + BalloonedMemory int32 + IpAddress []string + NumaNodeAffinity []string + StorageUsed int64 + ChangeTrackingEnabled bool + Devices []Device + NICs []NIC + Disks []VmDisk + Networks []VmNetwork +} + +// Virtual Disk. +type VmDisk struct { + FilePath string + Capacity string + CapacityAllocationUnits string + DiskId string + FileRef string + Format string + PopulatedSize string +} + +// Virtual Device. +type Device struct { + Kind string `json:"kind"` +} + +type Conf struct { + key string + Value string +} + +// Virtual ethernet card. +type NIC struct { + Name string `json:"name"` + MAC string `json:"mac"` + Config []Conf +} + +type VmNetwork struct { + Name string `xml:"name,attr"` + Description string `xml:"Description"` +} + +func main() { + + http.HandleFunc("/vms", vmHandler) + http.HandleFunc("/disks", diskHandler) + http.HandleFunc("/networks", networkHandler) + http.HandleFunc("/watch", watchdHandler) + + http.ListenAndServe(":8080", nil) + +} + +func vmHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + return + } + vmXML, ovfPath := scanOVAsOnNFS() + vmStruct, err := convertToVmStruct(vmXML, ovfPath) + if err != nil { + fmt.Println("Error processing OVF file:", err) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(vmStruct) +} + +func diskHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + return + } + xmlStruct, ovfPath := scanOVAsOnNFS() + diskStruct, err := convertToDiskStruct(xmlStruct, ovfPath) + if err != nil { + fmt.Println("Error processing OVF file:", err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(diskStruct) +} + +func networkHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + return + } + xmlStruct, _ := scanOVAsOnNFS() + netStruct, err := convertToNetworkStruct(xmlStruct) + if err != nil { + fmt.Println("Error processing OVF file:", err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(netStruct) +} + +func watchdHandler(w http.ResponseWriter, r *http.Request) { + fmt.Println(w, "This is the watch page!") + //TODO add watch +} + +func scanOVAsOnNFS() (envelopes []Envelope, ovaPaths []string) { + ovaFiles, err := findOVAFiles("/ova") + if err != nil { + fmt.Println("Error finding OVA files:", err) + return + } + + for _, ovaFile := range ovaFiles { + fmt.Println("Processing OVA file:", ovaFile) + + ovfPath, tmpDir, err := extractOVFFromOVA(ovaFile) + if err != nil { + fmt.Println("Error extracting OVF from OVA:", err) + continue + } + + xmlStruct, err := processOVF(ovfPath) + if err != nil { + fmt.Println("Error processing OVF file:", err) + os.RemoveAll(tmpDir) + } + + os.RemoveAll(tmpDir) + envelopes = append(envelopes, *xmlStruct) + ovaPaths = append(ovaPaths, ovfPath) + + } + return *&envelopes, ovaPaths +} + +func findOVAFiles(directory string) ([]string, error) { + var ovaFiles []string + + err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".ova") { + ovaFiles = append(ovaFiles, path) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return ovaFiles, nil +} + +func extractOVFFromOVA(ovaFile string) (string, string, error) { + tmpDir, err := ioutil.TempDir("", "ova_extraction") + print(tmpDir) + if err != nil { + os.RemoveAll(tmpDir) + return "", "", err + } + + cmd := exec.Command("tar", "-xf", ovaFile, "-C", tmpDir, "*.ovf") + err = cmd.Run() + if err != nil { + os.RemoveAll(tmpDir) + return "", "", err + } + + ovfFiles, err := filepath.Glob(filepath.Join(tmpDir, "*.ovf")) + if err != nil { + os.RemoveAll(tmpDir) + return "", "", err + } + + if len(ovfFiles) == 0 { + os.RemoveAll(tmpDir) + return "", "", fmt.Errorf("no OVF file found in the OVA") + } + + return ovfFiles[0], tmpDir, nil +} + +func processOVF(ovfPath string) (*Envelope, error) { + var envelope Envelope + + xmlFile, err := os.Open(ovfPath) + if err != nil { + return nil, err + } + defer xmlFile.Close() + + decoder := xml.NewDecoder(xmlFile) + + err = decoder.Decode(&envelope) + if err != nil { + return &envelope, err + } + + fmt.Println("Virtual Systems:") + for _, virtualSystem := range envelope.VirtualSystem { + fmt.Println("Virtual System ID:", virtualSystem.ID) + fmt.Println("Virtual System Name:", virtualSystem.Name) + fmt.Println("Operating System Description:", virtualSystem.OperatingSystemSection.Description) + fmt.Println("Virtual Hardware Info:", virtualSystem.HardwareSection.Info) + + fmt.Println("Virtual System Items:") + for _, item := range virtualSystem.HardwareSection.Items { + fmt.Printf("ElementName: %s, InstanceID: %s, ResourceType: %s\n", item.ElementName, item.InstanceID, item.ResourceType) + for _, conf := range item.Configs { + fmt.Printf("conf req: %s, key: %s, value: %s", conf.Required, conf.Key, conf.Value) + } + } + + fmt.Println("Disk Settings:") + for _, disk := range envelope.DiskSection.Disks { + fmt.Printf("DiskSize: %s, DiskId: %s\n", disk.Capacity, disk.DiskId) + } + + fmt.Println("Network Settings:") + for _, network := range envelope.NetworkSection.Networks { + fmt.Printf("NetworkName: %s, NetworkId: %s\n", network.Name, network.Description) + } + + fmt.Println("Config:") + for _, conf := range virtualSystem.HardwareSection.Configs { + fmt.Printf("conf req: %s, key: %s, value: %s", conf.Required, conf.Key, conf.Value) + } + fmt.Println() + } + + return &envelope, nil +} + +func convertToVmStruct(envelope []Envelope, ovaPath []string) ([]VM, error) { + var vms []VM + + for i := 0; i < len(envelope); i++ { + vmXml := envelope[i] + for _, virtualSystem := range vmXml.VirtualSystem { + + // Initialize a new VM + newVM := VM{ + OvaPath: ovaPath[i], + Name: virtualSystem.Name, + } + + for _, item := range virtualSystem.HardwareSection.Items { + if strings.Contains(item.ElementName, "Network adapter") { + newVM.NICs = append(newVM.NICs, NIC{ + Name: item.ElementName, + MAC: item.Address, + }) + //for _conf := range item. + } else if strings.Contains(item.Description, "Number of Virtual CPUs") { + newVM.CpuCount = item.VirtualQuantity + + } else if strings.Contains(item.Description, "Memory Size") { + newVM.MemoryMB = item.VirtualQuantity + + } else { + newVM.Devices = append(newVM.Devices, Device{ + Kind: item.ElementName[:len(item.ElementName)-2], + }) + } + + } + + for _, disk := range vmXml.DiskSection.Disks { + newVM.Disks = append(newVM.Disks, VmDisk{ + FilePath: ovaPath[i], + Capacity: disk.Capacity, + CapacityAllocationUnits: disk.CapacityAllocationUnits, + DiskId: disk.DiskId, + FileRef: disk.FileRef, + Format: disk.Format, + PopulatedSize: disk.PopulatedSize, + }) + } + + for _, network := range vmXml.NetworkSection.Networks { + newVM.Networks = append(newVM.Networks, VmNetwork{ + Name: network.Name, + Description: network.Description, + }) + } + + for _, conf := range virtualSystem.HardwareSection.Configs { + if conf.Key == "firmware" { + newVM.Firmware = conf.Value + } else if conf.Key == "memoryHotAddEnabled" { + newVM.MemoryHotAddEnabled, _ = strconv.ParseBool(conf.Value) + } else if conf.Key == "cpuHotAddEnabled" { + newVM.CpuHotAddEnabled, _ = strconv.ParseBool(conf.Value) + } else if conf.Key == "cpuHotRemoveEnabled" { + newVM.CpuHotRemoveEnabled, _ = strconv.ParseBool(conf.Value) + } + } + vms = append(vms, newVM) + } + } + return vms, nil +} + +func convertToNetworkStruct(envelope []Envelope) ([]VmNetwork, error) { + var networks []VmNetwork + for _, ova := range envelope { + for _, network := range ova.NetworkSection.Networks { + newNetwork := VmNetwork{ + Name: network.Name, + Description: network.Description, + } + networks = append(networks, newNetwork) + } + } + + return networks, nil +} + +func convertToDiskStruct(envelope []Envelope, ovaPath []string) ([]VmDisk, error) { + var disks []VmDisk + for i := 0; i < len(envelope); i++ { + ova := envelope[i] + for _, disk := range ova.DiskSection.Disks { + newDisk := VmDisk{ + FilePath: ovaPath[i], + Capacity: disk.Capacity, + CapacityAllocationUnits: disk.CapacityAllocationUnits, + DiskId: disk.DiskId, + FileRef: disk.FileRef, + Format: disk.Format, + PopulatedSize: disk.PopulatedSize, + } + + disks = append(disks, newDisk) + } + } + + return disks, nil +} diff --git a/hack/ova-parser/Containerfile b/hack/ova-parser/Containerfile new file mode 100644 index 000000000..2f800c7ad --- /dev/null +++ b/hack/ova-parser/Containerfile @@ -0,0 +1,10 @@ +FROM registry.access.redhat.com/ubi8/go-toolset:1.18.9-8 as builder +ENV GOPATH=$APP_ROOT +COPY . . + +RUN CGO_ENABLED=0 go build -o ova-parser github.com/konveyor/forklift-controller/cmd/ova-parser + +FROM quay.io/centos/centos:stream9-minimal +COPY --from=builder /opt/app-root/src/ova-parser /usr/local/bin + +ENTRYPOINT ["/usr/local/bin/ova-parser"] diff --git a/pkg/apis/forklift/v1beta1/provider.go b/pkg/apis/forklift/v1beta1/provider.go index 6b5852b68..1f9d4a2a5 100644 --- a/pkg/apis/forklift/v1beta1/provider.go +++ b/pkg/apis/forklift/v1beta1/provider.go @@ -40,6 +40,8 @@ const ( OVirt ProviderType = "ovirt" // OpenStack OpenStack ProviderType = "openstack" + // OVA + Ova ProviderType = "ova" ) var ProviderTypes = []ProviderType{ @@ -47,6 +49,7 @@ var ProviderTypes = []ProviderType{ VSphere, OVirt, OpenStack, + Ova, } func (t ProviderType) String() string { @@ -164,5 +167,5 @@ func (p *Provider) HasReconciled() bool { // This provider requires VM guest conversion. func (p *Provider) RequiresConversion() bool { - return p.Type() == VSphere + return p.Type() == VSphere || p.Type() == Ova } diff --git a/pkg/controller/provider/BUILD.bazel b/pkg/controller/provider/BUILD.bazel index 174c9f1d6..afa8d758c 100644 --- a/pkg/controller/provider/BUILD.bazel +++ b/pkg/controller/provider/BUILD.bazel @@ -29,7 +29,10 @@ go_library( "//pkg/settings", "//vendor/k8s.io/api/core/v1:core", "//vendor/k8s.io/apimachinery/pkg/api/errors", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:meta", "//vendor/k8s.io/apiserver/pkg/storage/names", + "//vendor/k8s.io/client-go/kubernetes", + "//vendor/k8s.io/client-go/tools/clientcmd", "//vendor/sigs.k8s.io/controller-runtime/pkg/client", "//vendor/sigs.k8s.io/controller-runtime/pkg/controller", "//vendor/sigs.k8s.io/controller-runtime/pkg/event", diff --git a/pkg/controller/provider/container/ova/doc.go b/pkg/controller/provider/container/ova/doc.go new file mode 100644 index 000000000..f20de7637 --- /dev/null +++ b/pkg/controller/provider/container/ova/doc.go @@ -0,0 +1 @@ +package ova diff --git a/pkg/controller/provider/controller.go b/pkg/controller/provider/controller.go index 1300999dd..150f6b1c7 100644 --- a/pkg/controller/provider/controller.go +++ b/pkg/controller/provider/controller.go @@ -18,6 +18,8 @@ package provider import ( "context" + "flag" + "fmt" "os" "path/filepath" "sync" @@ -38,8 +40,12 @@ import ( libref "github.com/konveyor/forklift-controller/pkg/lib/ref" "github.com/konveyor/forklift-controller/pkg/settings" core "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" k8serr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/storage/names" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -59,6 +65,17 @@ var log = logging.WithName(Name) // Application settings. var Settings = &settings.Settings +const ( + ovaContainerName = "ova" + ovaPodPrefix = "ova" + imageVar = "OVA_PARSER_IMAGE" + qemuGroup = 107 + nfsServer = "nfs-server.example.com" //remove + nfsPath = "/exported/path" //remove + nfsVolumeName = "nfs-volume" + mountPath = "/ova" +) + // Creates a new Inventory Controller and adds it to the Manager. func Add(mgr manager.Manager) error { libfb.WorkingDir = Settings.WorkingDir @@ -184,6 +201,55 @@ func (r Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (r } } + //if the provider added is ova we need to lunch a new pod + if provider.Type() == api.Ova { + podName := fmt.Sprintf("%s-%s", ovaPodPrefix, provider.UID) + imageName, ok := os.LookupEnv(imageVar) + if !ok { + r.Log.Info("Couldn't find", "imageVar", imageVar) + return + } + annotations := make(map[string]string) + labels := map[string]string{"providerName": provider.Name} + // Make the pod + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: provider.Namespace, + Annotations: annotations, + Labels: labels, + }, + Spec: makePOvaProviderPodSpec(), + } + con := &pod.Spec.Containers[0] + con.Image = imageName + + //figure out ho to get kubeocnfig + add volume with mount point for ova parser + var kubeconfig, masterURL string + if f := flag.Lookup("kubeconfig"); f != nil { + kubeconfig = f.Value.String() + } else { + flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") + } + flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") + + cfg, err1 := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig) + if err1 != nil { + r.Log.Error(err1, "Failed to create config: %v") + return + } + + kubeClient, err2 := kubernetes.NewForConfig(cfg) + if err2 != nil { + r.Log.Error(err, "Failed to create client") + } + + _, err = kubeClient.CoreV1().Pods(provider.Namespace).Create(ctx, pod, metav1.CreateOptions{}) + if err != nil { + return + } + } + // Begin staging conditions. provider.Status.Phase = Staging provider.Status.BeginStagingConditions() @@ -360,3 +426,49 @@ func (r *Catalog) get(request reconcile.Request) (p *api.Provider, found bool) { p, found = r.content[request] return } + +func makePOvaProviderPodSpec() corev1.PodSpec { + nonRoot := false + allowPrivilageEscalation := false + user := int64(qemuGroup) + + nfsVolumeSource := &corev1.NFSVolumeSource{ + Server: nfsServer, + Path: nfsPath, + } + + nfsVolume := corev1.Volume{ + Name: nfsVolumeName, + VolumeSource: corev1.VolumeSource{ + NFS: nfsVolumeSource, + }, + } + + volumeMountNfs := corev1.VolumeMount{ + Name: nfsVolumeName, + MountPath: mountPath, + } + + return corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: ovaContainerName, + Ports: []corev1.ContainerPort{{Name: "metrics", ContainerPort: 2112}}, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &allowPrivilageEscalation, + RunAsNonRoot: &nonRoot, + RunAsUser: &user, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + VolumeMounts: []corev1.VolumeMount{volumeMountNfs}, + }, + }, + Volumes: []corev1.Volume{nfsVolume}, + SecurityContext: &corev1.PodSecurityContext{ + FSGroup: &user, + }, + RestartPolicy: corev1.RestartPolicyNever, + } +} diff --git a/pkg/controller/provider/model/ova/BUILD.bazel b/pkg/controller/provider/model/ova/BUILD.bazel new file mode 100644 index 000000000..5b6908db8 --- /dev/null +++ b/pkg/controller/provider/model/ova/BUILD.bazel @@ -0,0 +1,18 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "ova", + srcs = [ + "doc.go", + "model.go", + "tree.go", + ], + importpath = "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova", + visibility = ["//visibility:public"], + deps = [ + "//pkg/controller/provider/model/base", + "//pkg/controller/provider/model/ocp", + "//pkg/lib/inventory/model", + "//pkg/lib/ref", + ], +) diff --git a/pkg/controller/provider/model/ova/doc.go b/pkg/controller/provider/model/ova/doc.go new file mode 100644 index 000000000..51b662591 --- /dev/null +++ b/pkg/controller/provider/model/ova/doc.go @@ -0,0 +1,14 @@ +package ova + +import ( + "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ocp" +) + +// Build all models. +func All() []interface{} { + return []interface{}{ + &ocp.Provider{}, + &VM{}, + &Network{}, + } +} diff --git a/pkg/controller/provider/model/ova/model.go b/pkg/controller/provider/model/ova/model.go new file mode 100644 index 000000000..9dcd4d243 --- /dev/null +++ b/pkg/controller/provider/model/ova/model.go @@ -0,0 +1,126 @@ +package ova + +import ( + "github.com/konveyor/forklift-controller/pkg/controller/provider/model/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" +) + +// Variants. +const ( + // Cluster. + ComputeResource = "ComputeResource" +) + +// Errors +var NotFound = libmodel.NotFound + +type InvalidRefError = base.InvalidRefError + +const ( + MaxDetail = base.MaxDetail +) + +// Types +type ListOptions = base.ListOptions +type Concern = base.Concern +type Ref = base.Ref + +// Model. +type Model interface { + base.Model + GetName() string +} + +// Base VMWare model. +type Base struct { + // Managed object ID. + ID string `sql:"pk"` + // Variant + Variant string `sql:"d0,index(variant)"` + // Name + Name string `sql:"d0,index(name)"` + // Revision + Revision int64 `sql:"incremented,d0,index(revision)"` +} + +func (m *Base) Pk() string { + return m.ID +} + +// String representation. +func (m *Base) String() string { + return m.ID +} + +// Get labels. +func (m *Base) Labels() libmodel.Labels { + return nil +} + +// Name. +func (m *Base) GetName() string { + return m.Name +} + +type Network struct { + Base + Name string + Description string +} + +type VM struct { + Base + Name string + OvaPath string + RevisionValidated int64 + PolicyVersion int + UUID string + Firmware string + CpuAffinity []int32 + CpuHotAddEnabled bool + CpuHotRemoveEnabled bool + MemoryHotAddEnabled bool + FaultToleranceEnabled bool + CpuCount int32 + CoresPerSocket int32 + MemoryMB int32 + BalloonedMemory int32 + IpAddress string + NumaNodeAffinity []string + StorageUsed int64 + ChangeTrackingEnabled bool + Devices []Device + NICs []NIC + Disks []Disk + Networks []Network + Concerns []Concern +} + +// Virtual Disk. +type Disk struct { + Base + FilePath string + Capacity string + CapacityAllocationUnits string + DiskId string + FileRef string + Format string + PopulatedSize string +} + +// Virtual Device. +type Device struct { + Kind string `json:"kind"` +} + +type Conf struct { + key string + Value string +} + +// Virtual ethernet card. +type NIC struct { + Name string `json:"name"` + MAC string `json:"mac"` + Config []Conf +} diff --git a/pkg/controller/provider/model/ova/tree.go b/pkg/controller/provider/model/ova/tree.go new file mode 100644 index 000000000..ba043c06d --- /dev/null +++ b/pkg/controller/provider/model/ova/tree.go @@ -0,0 +1,19 @@ +package ova + +import ( + "github.com/konveyor/forklift-controller/pkg/controller/provider/model/base" + libref "github.com/konveyor/forklift-controller/pkg/lib/ref" +) + +// Kinds +var ( + VmKind = libref.ToKind(VM{}) + NetKind = libref.ToKind(Network{}) + DiskKind = libref.ToKind(Disk{}) +) + +// Types. +type Tree = base.Tree +type TreeNode = base.TreeNode +type BranchNavigator = base.BranchNavigator +type ParentNavigator = base.ParentNavigator diff --git a/pkg/controller/provider/web/ova/BUILD.bazel b/pkg/controller/provider/web/ova/BUILD.bazel new file mode 100644 index 000000000..6edc2fee0 --- /dev/null +++ b/pkg/controller/provider/web/ova/BUILD.bazel @@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "ova", + srcs = [ + "base.go", + "client.go", + "disk.go", + "doc.go", + "network.go", + "provider.go", + "resource.go", + "tree.go", + "vm.go", + "workload.go", + ], + importpath = "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ova", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/forklift/v1beta1", + "//pkg/controller/provider/model/ocp", + "//pkg/controller/provider/model/ova", + "//pkg/controller/provider/web/base", + "//pkg/controller/provider/web/ocp", + "//pkg/lib/error", + "//pkg/lib/inventory/container", + "//pkg/lib/inventory/model", + "//pkg/lib/inventory/web", + "//pkg/lib/logging", + "//pkg/lib/ref", + "//vendor/github.com/gin-gonic/gin", + ], +) diff --git a/pkg/controller/provider/web/ova/base.go b/pkg/controller/provider/web/ova/base.go new file mode 100644 index 000000000..cf4687cb7 --- /dev/null +++ b/pkg/controller/provider/web/ova/base.go @@ -0,0 +1,65 @@ +package ova + +import ( + "strings" + + "github.com/gin-gonic/gin" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" + "github.com/konveyor/forklift-controller/pkg/lib/logging" +) + +// Package logger. +var log = logging.WithName("web|ova") + +// Fields. +const ( + DetailParam = base.DetailParam + NameParam = base.NameParam +) + +// Base handler. +type Handler struct { + base.Handler +} + +// Build list predicate. +func (h Handler) Predicate(ctx *gin.Context) (p libmodel.Predicate) { + q := ctx.Request.URL.Query() + name := q.Get(NameParam) + if len(name) > 0 { + path := strings.Split(name, "/") + name := path[len(path)-1] + p = libmodel.Eq(NameParam, name) + } + + return +} + +// Build list options. +func (h Handler) ListOptions(ctx *gin.Context) libmodel.ListOptions { + detail := h.Detail + if detail > 0 { + detail = model.MaxDetail + } + return libmodel.ListOptions{ + Predicate: h.Predicate(ctx), + Detail: detail, + Page: &h.Page, + } +} + +// Path builder. +type PathBuilder struct { + // Database. + DB libmodel.DB + // Cached resource + cache map[string]string +} + +func (r *PathBuilder) Path(m model.Model) (path string) { + + //TODO build object path + return +} diff --git a/pkg/controller/provider/web/ova/client.go b/pkg/controller/provider/web/ova/client.go new file mode 100644 index 000000000..b39b13e4f --- /dev/null +++ b/pkg/controller/provider/web/ova/client.go @@ -0,0 +1,294 @@ +package ova + +import ( + "strings" + + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + liberr "github.com/konveyor/forklift-controller/pkg/lib/error" +) + +// Errors. +type ResourceNotResolvedError = base.ResourceNotResolvedError +type RefNotUniqueError = base.RefNotUniqueError +type NotFoundError = base.NotFoundError + +// API path resolver. +type Resolver struct { + *api.Provider +} + +// Build the URL path. +func (r *Resolver) Path(resource interface{}, id string) (path string, err error) { + provider := r.Provider + switch resource.(type) { + case *Provider: + r := Provider{} + r.UID = id + r.Link() + path = r.SelfLink + case *Network: + r := Network{} + r.ID = id + r.Link(provider) + path = r.SelfLink + case *VM: + r := VM{} + r.ID = id + r.Link(provider) + path = r.SelfLink + case *Disk: + r := Disk{} + r.ID = id + r.Link(provider) + path = r.SelfLink + case *Workload: + r := Workload{} + r.ID = id + r.Link(provider) + path = r.SelfLink + default: + err = liberr.Wrap( + base.ResourceNotResolvedError{ + Object: resource, + }) + } + + path = strings.TrimRight(path, "/") + + return +} + +// Resource finder. +type Finder struct { + base.Client +} + +// With client. +func (r *Finder) With(client base.Client) base.Finder { + r.Client = client + return r +} + +// Find a resource by ref. +// Returns: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) ByRef(resource interface{}, ref base.Ref) (err error) { + switch resource.(type) { + case *Network: + id := ref.ID + if id != "" { + err = r.Get(resource, id) + return + } + name := ref.Name + if name != "" { + list := []Network{} + err = r.List( + &list, + base.Param{ + Key: DetailParam, + Value: "all", + }, + base.Param{ + Key: NameParam, + Value: name, + }) + if err != nil { + break + } + if len(list) == 0 { + err = liberr.Wrap(NotFoundError{Ref: ref}) + break + } + if len(list) > 1 { + err = liberr.Wrap(RefNotUniqueError{Ref: ref}) + break + } + *resource.(*Network) = list[0] + } + case *VM: + id := ref.ID + if id != "" { + err = r.Get(resource, id) + return + } + name := ref.Name + if name != "" { + list := []VM{} + err = r.List( + &list, + base.Param{ + Key: DetailParam, + Value: "all", + }, + base.Param{ + Key: NameParam, + Value: name, + }) + if err != nil { + break + } + if len(list) == 0 { + err = liberr.Wrap(NotFoundError{Ref: ref}) + break + } + if len(list) > 1 { + err = liberr.Wrap(RefNotUniqueError{Ref: ref}) + break + } + *resource.(*VM) = list[0] + } + case *Disk: + id := ref.ID + if id != "" { + err = r.Get(resource, id) + return + } + name := ref.Name + if name != "" { + list := []Disk{} + err = r.List( + &list, + base.Param{ + Key: DetailParam, + Value: "all", + }, + base.Param{ + Key: NameParam, + Value: name, + }) + if err != nil { + break + } + if len(list) == 0 { + err = liberr.Wrap(NotFoundError{Ref: ref}) + break + } + if len(list) > 1 { + err = liberr.Wrap(RefNotUniqueError{Ref: ref}) + break + } + *resource.(*Disk) = list[0] + } + case *Workload: + id := ref.ID + if id != "" { + err = r.Get(resource, id) + return + } + name := ref.Name + if name != "" { + list := []Workload{} + err = r.List( + &list, + base.Param{ + Key: DetailParam, + Value: "all", + }, + base.Param{ + Key: NameParam, + Value: name, + }) + if err != nil { + break + } + if len(list) == 0 { + err = liberr.Wrap(NotFoundError{Ref: ref}) + break + } + if len(list) > 1 { + err = liberr.Wrap(RefNotUniqueError{Ref: ref}) + break + } + *resource.(*Workload) = list[0] + } + default: + err = liberr.Wrap( + ResourceNotResolvedError{ + Object: resource, + }) + } + + return +} + +// Find a VM by ref. +// Returns the matching resource and: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) VM(ref *base.Ref) (object interface{}, err error) { + vm := &VM{} + err = r.ByRef(vm, *ref) + if err == nil { + ref.ID = vm.ID + ref.Name = vm.Name + object = vm + } + + return +} + +// Find a Network by ref. +// Returns the matching resource and: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) Network(ref *base.Ref) (object interface{}, err error) { + network := &Network{} + err = r.ByRef(network, *ref) + if err == nil { + ref.ID = network.ID + ref.Name = network.Name + object = network + } + + return +} + +// Find a Disk by ref. +// Returns the matching resource and: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) Disk(ref *base.Ref) (object interface{}, err error) { + disk := &Disk{} + err = r.ByRef(disk, *ref) + if err == nil { + ref.ID = disk.ID + ref.Name = disk.Name + object = disk + } + + return +} + +// Find workload by ref. +// Returns the matching resource and: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) Workload(ref *base.Ref) (object interface{}, err error) { + workload := &Workload{} + err = r.ByRef(workload, *ref) + if err == nil { + ref.ID = workload.ID + ref.Name = workload.Name + object = workload + } + + return +} diff --git a/pkg/controller/provider/web/ova/disk.go b/pkg/controller/provider/web/ova/disk.go new file mode 100644 index 000000000..5ce6de3e1 --- /dev/null +++ b/pkg/controller/provider/web/ova/disk.go @@ -0,0 +1,172 @@ +package ova + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" +) + +// Routes +const ( + DiskParam = "disk" + DiskCollection = "disks" + DisksRoot = ProviderRoot + "/" + DiskCollection + DiskRoot = DisksRoot + "/:" + DiskParam +) + +// Disk handler. +type DiskHandler struct { + Handler +} + +// Add routes to the `gin` router. +func (h *DiskHandler) AddRoutes(e *gin.Engine) { + e.GET(DisksRoot, h.List) + e.GET(DisksRoot+"/", h.List) + e.GET(DiskRoot, h.Get) +} + +// List resources in a REST collection. +// A GET onn the collection that includes the `X-Watch` +// header will negotiate an upgrade of the connection +// to a websocket and push watch events. +func (h DiskHandler) List(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.WatchRequest { + h.watch(ctx) + return + } + db := h.Collector.DB() + list := []model.Disk{} + err = db.List(&list, h.ListOptions(ctx)) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + content := []interface{}{} + for _, m := range list { + r := &Disk{} + r.With(&m) + r.Link(h.Provider) + content = append(content, r.Content(h.Detail)) + } + + ctx.JSON(http.StatusOK, content) +} + +// Get a specific REST resource. +func (h DiskHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + h.Detail = model.MaxDetail + m := &model.Disk{ + Base: model.Base{ + ID: ctx.Param(DiskParam), + }, + } + db := h.Collector.DB() + err = db.Get(m) + if errors.Is(err, model.NotFound) { + ctx.Status(http.StatusNotFound) + return + } + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + r := &Disk{} + r.With(m) + r.Link(h.Provider) + content := r.Content(h.Detail) + + ctx.JSON(http.StatusOK, content) +} + +// Watch. +func (h *DiskHandler) watch(ctx *gin.Context) { + db := h.Collector.DB() + err := h.Watch( + ctx, + db, + &model.Disk{}, + func(in libmodel.Model) (r interface{}) { + m := in.(*model.Disk) + disk := &Disk{} + disk.With(m) + disk.Link(h.Provider) + r = disk + return + }) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } +} + +// REST Resource. +type Disk struct { + Resource + FilePath string + Capacity string + CapacityAllocationUnits string + DiskId string + FileRef string + Format string + PopulatedSize string +} + +// Build the resource using the model. +func (r *Disk) With(m *model.Disk) { + r.Resource.With(&m.Base) + r.FilePath = m.FilePath + r.Capacity = m.Capacity + r.CapacityAllocationUnits = m.CapacityAllocationUnits + r.DiskId = m.DiskId + r.FileRef = m.FileRef + r.Format = m.Format + r.PopulatedSize = m.PopulatedSize +} + +// Build self link (URI). +func (r *Disk) Link(p *api.Provider) { + r.SelfLink = base.Link( + DiskRoot, + base.Params{ + base.ProviderParam: string(p.UID), + DiskParam: r.ID, + }) +} + +// As content. +func (r *Disk) Content(detail int) interface{} { + if detail == 0 { + return r.Resource + } + + return r +} diff --git a/pkg/controller/provider/web/ova/doc.go b/pkg/controller/provider/web/ova/doc.go new file mode 100644 index 000000000..381976147 --- /dev/null +++ b/pkg/controller/provider/web/ova/doc.go @@ -0,0 +1,49 @@ +package ova + +import ( + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + "github.com/konveyor/forklift-controller/pkg/lib/inventory/container" + libweb "github.com/konveyor/forklift-controller/pkg/lib/inventory/web" +) + +// Routes +const ( + Root = base.ProvidersRoot + "/" + string(api.Ova) +) + +// Build all handlers. +func Handlers(container *container.Container) []libweb.RequestHandler { + return []libweb.RequestHandler{ + &ProviderHandler{ + Handler: base.Handler{ + Container: container, + }, + }, + &TreeHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + &DiskHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + &NetworkHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + &VMHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + &WorkloadHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + } +} diff --git a/pkg/controller/provider/web/ova/network.go b/pkg/controller/provider/web/ova/network.go new file mode 100644 index 000000000..b107fed35 --- /dev/null +++ b/pkg/controller/provider/web/ova/network.go @@ -0,0 +1,210 @@ +package ova + +import ( + "errors" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" +) + +// Routes. +const ( + NetworkParam = "network" + NetworkCollection = "networks" + NetworksRoot = ProviderRoot + "/" + NetworkCollection + NetworkRoot = NetworksRoot + "/:" + NetworkParam +) + +// Network handler. +type NetworkHandler struct { + Handler +} + +// Add routes to the `gin` router. +func (h *NetworkHandler) AddRoutes(e *gin.Engine) { + e.GET(NetworksRoot, h.List) + e.GET(NetworksRoot+"/", h.List) + e.GET(NetworkRoot, h.Get) +} + +// List resources in a REST collection. +// A GET onn the collection that includes the `X-Watch` +// header will negotiate an upgrade of the connection +// to a websocket and push watch events. +func (h NetworkHandler) List(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.WatchRequest { + h.watch(ctx) + return + } + defer func() { + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } + }() + db := h.Collector.DB() + list := []model.Network{} + err = db.List(&list, h.ListOptions(ctx)) + if err != nil { + return + } + err = h.filter(ctx, &list) + if err != nil { + return + } + pb := PathBuilder{DB: db} + content := []interface{}{} + for _, m := range list { + r := &Network{} + r.With(&m) + r.Link(h.Provider) + r.Path = pb.Path(&m) + content = append(content, r.Content(h.Detail)) + } + + ctx.JSON(http.StatusOK, content) +} + +// Get a specific REST resource. +func (h NetworkHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + m := &model.Network{ + Base: model.Base{ + ID: ctx.Param(NetworkParam), + }, + } + db := h.Collector.DB() + err = db.Get(m) + if errors.Is(err, model.NotFound) { + ctx.Status(http.StatusNotFound) + return + } + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + pb := PathBuilder{DB: db} + r := &Network{} + r.With(m) + r.Link(h.Provider) + r.Path = pb.Path(m) + content := r.Content(model.MaxDetail) + + ctx.JSON(http.StatusOK, content) +} + +// Watch. +func (h *NetworkHandler) watch(ctx *gin.Context) { + db := h.Collector.DB() + err := h.Watch( + ctx, + db, + &model.Network{}, + func(in libmodel.Model) (r interface{}) { + pb := PathBuilder{DB: db} + m := in.(*model.Network) + network := &Network{} + network.With(m) + network.Link(h.Provider) + network.Path = pb.Path(m) + r = network + return + }) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } +} + +// Filter result set. +// Filter by path for `name` query. +func (h *NetworkHandler) filter(ctx *gin.Context, list *[]model.Network) (err error) { + if len(*list) < 2 { + return + } + q := ctx.Request.URL.Query() + name := q.Get(NameParam) + if len(name) == 0 { + return + } + if len(strings.Split(name, "/")) < 2 { + return + } + db := h.Collector.DB() + pb := PathBuilder{DB: db} + kept := []model.Network{} + for _, m := range *list { + path := pb.Path(&m) + if h.PathMatchRoot(path, name) { + kept = append(kept, m) + } + } + + *list = kept + + return +} + +// REST Resource. +type Network struct { + Resource + Name string + Description string +} + +// Build the resource using the model. +func (r *Network) With(m *model.Network) { + r.Resource.With(&m.Base) + r.Variant = m.Variant + // switch m.Variant { + // case model.Name: + // r.Name = m.Name + // case model.Description: + // r.Description = &m.Description + // } +} + +// Build self link (URI). +func (r *Network) Link(p *api.Provider) { + r.SelfLink = base.Link( + NetworkRoot, + base.Params{ + base.ProviderParam: string(p.UID), + NetworkParam: r.ID, + }) +} + +// As content. +func (r *Network) Content(detail int) interface{} { + if detail == 0 { + return r.Resource + } + + return r +} diff --git a/pkg/controller/provider/web/ova/provider.go b/pkg/controller/provider/web/ova/provider.go new file mode 100644 index 000000000..966664938 --- /dev/null +++ b/pkg/controller/provider/web/ova/provider.go @@ -0,0 +1,192 @@ +package ova + +import ( + "net/http" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ocp" + "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ocp" +) + +// Routes. +const ( + ProviderParam = base.ProviderParam + ProvidersRoot = Root + ProviderRoot = ProvidersRoot + "/:" + ProviderParam +) + +// Provider handler. +type ProviderHandler struct { + base.Handler +} + +// Add routes to the `gin` router. +func (h *ProviderHandler) AddRoutes(e *gin.Engine) { + e.GET(ProvidersRoot, h.List) + e.GET(ProvidersRoot+"/", h.List) + e.GET(ProviderRoot, h.Get) +} + +// List resources in a REST collection. +func (h ProviderHandler) List(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.WatchRequest { + ctx.Status(http.StatusBadRequest) + return + } + content, err := h.ListContent(ctx) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + + ctx.JSON(http.StatusOK, content) +} + +// Get a specific REST resource. +func (h ProviderHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.Provider.Type() != api.Ova { + ctx.Status(http.StatusNotFound) + return + } + h.Detail = model.MaxDetail + m := &model.Provider{} + m.With(h.Provider) + r := Provider{} + r.With(m) + err = h.AddDerived(&r) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + r.Link() + content := r.Content(h.Detail) + + ctx.JSON(http.StatusOK, content) +} + +// Build the list content. +func (h *ProviderHandler) ListContent(ctx *gin.Context) (content []interface{}, err error) { + content = []interface{}{} + list := h.Container.List() + q := ctx.Request.URL.Query() + ns := q.Get(base.NsParam) + for _, collector := range list { + if p, cast := collector.Owner().(*api.Provider); cast { + if p.Type() != api.Ova { + continue + } + if ns != "" && ns != p.Namespace { + continue + } + if collector, found := h.Container.Get(p); found { + h.Collector = collector + } else { + continue + } + m := &model.Provider{} + m.With(p) + r := Provider{} + r.With(m) + aErr := h.AddDerived(&r) + if aErr != nil { + err = aErr + return + } + r.Link() + content = append(content, r.Content(h.Detail)) + } + } + + h.Page.Slice(&content) + + return +} + +// Add derived fields. +func (h ProviderHandler) AddDerived(r *Provider) (err error) { + var n int64 + if h.Detail == 0 { + return + } + db := h.Collector.DB() + // VM + n, err = db.Count(&ova.VM{}, nil) + if err != nil { + return + } + r.VMCount = n + // Network + n, err = db.Count(&ova.Network{}, nil) + if err != nil { + return + } + r.NetworkCount = n + // Disk + n, err = db.Count(&ova.Disk{}, nil) + if err != nil { + return + } + r.DiskCount = n + + return +} + +// REST Resource. +type Provider struct { + ocp.Resource + Type string `json:"type"` + Object api.Provider `json:"object"` + APIVersion string `json:"apiVersion"` + Product string `json:"product"` + VMCount int64 `json:"vmCount"` + NetworkCount int64 `json:"networkCount"` + DiskCount int64 +} + +// Set fields with the specified object. +func (r *Provider) With(m *model.Provider) { + r.Resource.With(&m.Base) + r.Type = m.Type + r.Object = m.Object +} + +// Build self link (URI). +func (r *Provider) Link() { + r.SelfLink = base.Link( + ProviderRoot, + base.Params{ + base.ProviderParam: r.UID, + }) +} + +// As content. +func (r *Provider) Content(detail int) interface{} { + if detail == 0 { + return r.Resource + } + + return r +} diff --git a/pkg/controller/provider/web/ova/resource.go b/pkg/controller/provider/web/ova/resource.go new file mode 100644 index 000000000..3d6cbb3f3 --- /dev/null +++ b/pkg/controller/provider/web/ova/resource.go @@ -0,0 +1,29 @@ +package ova + +import ( + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" +) + +// REST Resource. +type Resource struct { + // Object ID. + ID string `json:"id"` + // Variant + Variant string `json:"variant,omitempty"` + // Path + Path string `json:"path,omitempty"` + // Revision + Revision int64 `json:"revision"` + // Object name. + Name string `json:"name"` + // Self link. + SelfLink string `json:"selfLink"` +} + +// Build the resource using the model. +func (r *Resource) With(m *model.Base) { + r.ID = m.ID + r.Variant = m.Variant + r.Revision = m.Revision + r.Name = m.Name +} diff --git a/pkg/controller/provider/web/ova/vm.go b/pkg/controller/provider/web/ova/vm.go new file mode 100644 index 000000000..e8c192b2d --- /dev/null +++ b/pkg/controller/provider/web/ova/vm.go @@ -0,0 +1,274 @@ +package ova + +import ( + "errors" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" +) + +// Routes. +const ( + VMParam = "vm" + VMCollection = "vms" + VMsRoot = ProviderRoot + "/" + VMCollection + VMRoot = VMsRoot + "/:" + VMParam +) + +// Virtual Machine handler. +type VMHandler struct { + Handler +} + +// Add routes to the `gin` router. +func (h *VMHandler) AddRoutes(e *gin.Engine) { + e.GET(VMsRoot, h.List) + e.GET(VMsRoot+"/", h.List) + e.GET(VMRoot, h.Get) +} + +// List resources in a REST collection. +// A GET onn the collection that includes the `X-Watch` +// header will negotiate an upgrade of the connection +// to a websocket and push watch events. +func (h VMHandler) List(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.WatchRequest { + h.watch(ctx) + return + } + defer func() { + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } + }() + db := h.Collector.DB() + list := []model.VM{} + err = db.List(&list, h.ListOptions(ctx)) + if err != nil { + return + } + content := []interface{}{} + err = h.filter(ctx, &list) + if err != nil { + return + } + pb := PathBuilder{DB: db} + for _, m := range list { + r := &VM{} + r.With(&m) + r.Link(h.Provider) + r.Path = pb.Path(&m) + content = append(content, r.Content(h.Detail)) + } + + ctx.JSON(http.StatusOK, content) +} + +// Get a specific REST resource. +func (h VMHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + m := &model.VM{ + Base: model.Base{ + ID: ctx.Param(VMParam), + }, + } + db := h.Collector.DB() + err = db.Get(m) + if errors.Is(err, model.NotFound) { + ctx.Status(http.StatusNotFound) + return + } + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + pb := PathBuilder{DB: db} + r := &VM{} + r.With(m) + r.Link(h.Provider) + r.Path = pb.Path(m) + content := r.Content(model.MaxDetail) + + ctx.JSON(http.StatusOK, content) +} + +// Watch. +func (h *VMHandler) watch(ctx *gin.Context) { + db := h.Collector.DB() + err := h.Watch( + ctx, + db, + &model.VM{}, + func(in libmodel.Model) (r interface{}) { + pb := PathBuilder{DB: db} + m := in.(*model.VM) + vm := &VM{} + vm.With(m) + vm.Link(h.Provider) + vm.Path = pb.Path(m) + r = vm + return + }) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } +} + +// Filter result set. +// Filter by path for `name` query. +func (h *VMHandler) filter(ctx *gin.Context, list *[]model.VM) (err error) { + if len(*list) < 2 { + return + } + q := ctx.Request.URL.Query() + name := q.Get(NameParam) + if len(name) == 0 { + return + } + if len(strings.Split(name, "/")) < 2 { + return + } + db := h.Collector.DB() + pb := PathBuilder{DB: db} + kept := []model.VM{} + for _, m := range *list { + path := pb.Path(&m) + if h.PathMatch(path, name) { + kept = append(kept, m) + } + } + + *list = kept + + return +} + +// VM detail=0 +type VM0 = Resource + +// VM detail=1 +type VM1 struct { + VM0 + RevisionValidated int64 `json:"revisionValidated"` + Networks []model.Network `json:"networks"` + Disks []model.Disk `json:"disks"` + Concerns []model.Concern `json:"concerns"` +} + +// Build the resource using the model. +func (r *VM1) With(m *model.VM) { + r.VM0.With(&m.Base) + r.RevisionValidated = m.RevisionValidated + r.Disks = m.Disks + r.Concerns = m.Concerns +} + +// As content. +func (r *VM1) Content(detail int) interface{} { + if detail < 1 { + return &r.VM0 + } + + return r +} + +// VM full detail. +type VM struct { + VM1 + Name string + OvaPath string + RevisionValidated int64 + PolicyVersion int + UUID string + Firmware string + CpuAffinity []int32 + CpuHotAddEnabled bool + CpuHotRemoveEnabled bool + MemoryHotAddEnabled bool + FaultToleranceEnabled bool + CpuCount int32 + CoresPerSocket int32 + MemoryMB int32 + BalloonedMemory int32 + IpAddress string + NumaNodeAffinity []string + StorageUsed int64 + ChangeTrackingEnabled bool + Devices []model.Device + NICs []model.NIC + Disks []model.Disk + Networks []model.Network +} + +// Build the resource using the model. +func (r *VM) With(m *model.VM) { + r.VM1.With(m) + r.PolicyVersion = m.PolicyVersion + r.UUID = m.UUID + r.Firmware = m.Firmware + r.ChangeTrackingEnabled = m.ChangeTrackingEnabled + r.CpuAffinity = m.CpuAffinity + r.CpuHotAddEnabled = m.CpuHotAddEnabled + r.CpuHotRemoveEnabled = m.CpuHotRemoveEnabled + r.MemoryHotAddEnabled = m.MemoryHotAddEnabled + r.CpuCount = m.CpuCount + r.CoresPerSocket = m.CoresPerSocket + r.MemoryMB = m.MemoryMB + r.BalloonedMemory = m.BalloonedMemory + r.IpAddress = m.IpAddress + r.StorageUsed = m.StorageUsed + r.FaultToleranceEnabled = m.FaultToleranceEnabled + r.Devices = m.Devices + r.NumaNodeAffinity = m.NumaNodeAffinity + r.NICs = m.NICs + r.OvaPath = m.OvaPath + r.Disks = m.Disks + r.Networks = m.Networks +} + +// Build self link (URI). +func (r *VM) Link(p *api.Provider) { + r.SelfLink = base.Link( + VMRoot, + base.Params{ + base.ProviderParam: string(p.UID), + VMParam: r.ID, + }) +} + +// As content. +func (r *VM) Content(detail int) interface{} { + if detail < 2 { + return r.VM1.Content(detail) + } + + return r +} diff --git a/pkg/controller/provider/web/ova/workload.go b/pkg/controller/provider/web/ova/workload.go new file mode 100644 index 000000000..791200c3c --- /dev/null +++ b/pkg/controller/provider/web/ova/workload.go @@ -0,0 +1,91 @@ +package ova + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" +) + +// Routes. +const ( + WorkloadCollection = "workloads" + WorkloadsRoot = ProviderRoot + "/" + WorkloadCollection + WorkloadRoot = WorkloadsRoot + "/:" + VMParam +) + +// Virtual Machine handler. +type WorkloadHandler struct { + Handler +} + +// Add routes to the `gin` router. +func (h *WorkloadHandler) AddRoutes(e *gin.Engine) { + e.GET(WorkloadRoot, h.Get) +} + +// List resources in a REST collection. +func (h WorkloadHandler) List(ctx *gin.Context) { +} + +// Get a specific REST resource. +func (h WorkloadHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + m := &model.VM{ + Base: model.Base{ + ID: ctx.Param(VMParam), + }, + } + db := h.Collector.DB() + err = db.Get(m) + if errors.Is(err, model.NotFound) { + ctx.Status(http.StatusNotFound) + return + } + defer func() { + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } + }() + if err != nil { + return + } + r := Workload{} + r.With(m) + r.Link(h.Provider) + content := r + + ctx.JSON(http.StatusOK, content) +} + +// Workload +type Workload struct { + SelfLink string `json:"selfLink"` + VM +} + +func (r *Workload) With(m *model.VM) { + r.VM.With(m) +} + +// Build self link (URI). +func (r *Workload) Link(p *api.Provider) { + r.SelfLink = base.Link( + WorkloadRoot, + base.Params{ + base.ProviderParam: string(p.UID), + VMParam: r.ID, + }) +} diff --git a/pkg/forklift-api/webhooks/validating-webhook/admitters/provider-admitter.go b/pkg/forklift-api/webhooks/validating-webhook/admitters/provider-admitter.go index 514fcb266..0ffdd4b98 100644 --- a/pkg/forklift-api/webhooks/validating-webhook/admitters/provider-admitter.go +++ b/pkg/forklift-api/webhooks/validating-webhook/admitters/provider-admitter.go @@ -6,17 +6,16 @@ import ( "github.com/konveyor/forklift-controller/pkg/apis" api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" - liberr "github.com/konveyor/forklift-controller/pkg/lib/error" "github.com/konveyor/forklift-controller/pkg/forklift-api/webhooks/util" + liberr "github.com/konveyor/forklift-controller/pkg/lib/error" admissionv1 "k8s.io/api/admission/v1beta1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" ) - type ProviderAdmitter struct { - client client.Client + client client.Client provider api.Provider } @@ -58,7 +57,7 @@ func (admitter *ProviderAdmitter) validateVDDK() error { context.TODO(), client.ObjectKey{ Namespace: plan.Spec.Provider.Destination.Namespace, - Name: plan.Spec.Provider.Destination.Name, + Name: plan.Spec.Provider.Destination.Name, }, &destinationProvider) if err != nil { diff --git a/tests/suit/BUILD.bazel b/tests/suit/BUILD.bazel index e0083937f..c97734224 100644 --- a/tests/suit/BUILD.bazel +++ b/tests/suit/BUILD.bazel @@ -58,6 +58,7 @@ go_test( name = "suit_test", srcs = [ "forklift_test.go", + "openstack_extended_test.go", "openstack_test.go", "ovirt_test.go", "tests_suite_test.go", @@ -65,6 +66,8 @@ go_test( ], deps = [ "//pkg/apis/forklift/v1beta1", + "//pkg/controller/provider/container/openstack", + "//pkg/lib/logging", "//tests/suit/framework", "//tests/suit/utils", "//vendor/github.com/onsi/ginkgo", diff --git a/tests/suit/utils/BUILD.bazel b/tests/suit/utils/BUILD.bazel index fbe767e3a..d17efd9e6 100644 --- a/tests/suit/utils/BUILD.bazel +++ b/tests/suit/utils/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "utils", srcs = [ "common.go", + "http.go", "migration.go", "networkmap.go", "plan.go", @@ -19,6 +20,7 @@ go_library( "//pkg/apis/forklift/v1beta1/plan", "//pkg/apis/forklift/v1beta1/provider", "//pkg/apis/forklift/v1beta1/ref", + "//pkg/lib/error", "//vendor/github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1:k8s_cni_cncf_io", "//vendor/github.com/onsi/ginkgo", "//vendor/github.com/pkg/errors",