diff --git a/cmd/cluster-node-tuning-operator/main.go b/cmd/cluster-node-tuning-operator/main.go index e1e18a1e4f..c9d7106f76 100644 --- a/cmd/cluster-node-tuning-operator/main.go +++ b/cmd/cluster-node-tuning-operator/main.go @@ -49,6 +49,7 @@ import ( "github.com/openshift/cluster-node-tuning-operator/pkg/performanceprofile/cmd/render" "github.com/openshift/cluster-node-tuning-operator/pkg/signals" "github.com/openshift/cluster-node-tuning-operator/pkg/tuned" + tunedrender "github.com/openshift/cluster-node-tuning-operator/pkg/tuned/cmd/render" "github.com/openshift/cluster-node-tuning-operator/pkg/util" "github.com/openshift/cluster-node-tuning-operator/version" ) @@ -107,6 +108,7 @@ func prepareCommands() { if !config.InHyperShift() { rootCmd.AddCommand(render.NewRenderCommand()) + rootCmd.AddCommand(tunedrender.NewRenderBootCmdMCCommand()) } } diff --git a/pkg/operator/controller.go b/pkg/operator/controller.go index 14a5fec333..83b42a6901 100644 --- a/pkg/operator/controller.go +++ b/pkg/operator/controller.go @@ -782,7 +782,7 @@ func (c *Controller) syncMachineConfig(pools []*mcfgv1.MachineConfigPool, labels kernelArguments []string ) - name := getMachineConfigNameForPools(pools) + name := GetMachineConfigNameForPools(pools) nodes, err := c.pc.getNodesForPool(pools[0]) if err != nil { return err @@ -812,18 +812,18 @@ func (c *Controller) syncMachineConfig(pools []*mcfgv1.MachineConfigPool, labels klog.V(2).Infof("not creating a MachineConfig with empty kernelArguments") return nil } - mc = newMachineConfig(name, annotations, labels, kernelArguments) + mc = NewMachineConfig(name, annotations, labels, kernelArguments) _, err = c.clients.MC.MachineconfigurationV1().MachineConfigs().Create(context.TODO(), mc, metav1.CreateOptions{}) if err != nil { return fmt.Errorf("failed to create MachineConfig %s: %v", mc.ObjectMeta.Name, err) } - klog.Infof("created MachineConfig %s with%s", mc.ObjectMeta.Name, machineConfigGenerationLogLine(len(bootcmdline) != 0, bootcmdline)) + klog.Infof("created MachineConfig %s with%s", mc.ObjectMeta.Name, MachineConfigGenerationLogLine(len(bootcmdline) != 0, bootcmdline)) return nil } return err } - mcNew := newMachineConfig(name, annotations, labels, kernelArguments) + mcNew := NewMachineConfig(name, annotations, labels, kernelArguments) kernelArgsEq := util.StringSlicesEqual(mc.Spec.KernelArguments, kernelArguments) if kernelArgsEq { @@ -836,7 +836,7 @@ func (c *Controller) syncMachineConfig(pools []*mcfgv1.MachineConfigPool, labels mc.Spec.KernelArguments = kernelArguments mc.Spec.Config = mcNew.Spec.Config - l := machineConfigGenerationLogLine(!kernelArgsEq, bootcmdline) + l := MachineConfigGenerationLogLine(!kernelArgsEq, bootcmdline) klog.V(2).Infof("syncMachineConfig(): updating MachineConfig %s with%s", mc.ObjectMeta.Name, l) _, err = c.clients.MC.MachineconfigurationV1().MachineConfigs().Update(context.TODO(), mc, metav1.UpdateOptions{}) if err != nil { @@ -900,7 +900,7 @@ func (c *Controller) syncMachineConfigHyperShift(nodePoolName string, profile *t klog.V(2).Infof("not creating a MachineConfig with empty kernelArguments") return nil } - mc := newMachineConfig(mcName, annotations, nil, kernelArguments) + mc := NewMachineConfig(mcName, annotations, nil, kernelArguments) // put the MC into a ConfigMap and create that instead mcConfigMap, err = newConfigMapForMachineConfig(configMapName, nodePoolName, mc) @@ -912,7 +912,7 @@ func (c *Controller) syncMachineConfigHyperShift(nodePoolName string, profile *t if err != nil { return fmt.Errorf("failed to create ConfigMap %s for MachineConfig %s: %v", configMapName, mc.ObjectMeta.Name, err) } - klog.Infof("created ConfigMap %s for MachineConfig %s with%s", configMapName, mc.ObjectMeta.Name, machineConfigGenerationLogLine(len(bootcmdline) != 0, bootcmdline)) + klog.Infof("created ConfigMap %s for MachineConfig %s with%s", configMapName, mc.ObjectMeta.Name, MachineConfigGenerationLogLine(len(bootcmdline) != 0, bootcmdline)) return nil } return err @@ -926,7 +926,7 @@ func (c *Controller) syncMachineConfigHyperShift(nodePoolName string, profile *t return nil } - mcNew := newMachineConfig(mcName, annotations, nil, kernelArguments) + mcNew := NewMachineConfig(mcName, annotations, nil, kernelArguments) // Compare kargs between existing and new mcfg kernelArgsEq := util.StringSlicesEqual(mc.Spec.KernelArguments, kernelArguments) @@ -951,7 +951,7 @@ func (c *Controller) syncMachineConfigHyperShift(nodePoolName string, profile *t mc.Spec.KernelArguments = kernelArguments mc.Spec.Config = mcNew.Spec.Config - l := machineConfigGenerationLogLine(!kernelArgsEq, bootcmdline) + l := MachineConfigGenerationLogLine(!kernelArgsEq, bootcmdline) klog.V(2).Infof("syncMachineConfig(): updating MachineConfig %s with%s", mc.ObjectMeta.Name, l) newData, err := serializeMachineConfig(mc) @@ -1078,7 +1078,7 @@ func (c *Controller) getMachineConfigNamesForTuned() (map[string]bool, error) { mcNames := map[string]bool{} - for _, recommend := range tunedRecommend(tunedList) { + for _, recommend := range TunedRecommend(tunedList) { if recommend.Profile == nil || recommend.MachineConfigLabels == nil { continue } @@ -1087,7 +1087,7 @@ func (c *Controller) getMachineConfigNamesForTuned() (map[string]bool, error) { if err != nil { return nil, err } - mcName := getMachineConfigNameForPools(pools) + mcName := GetMachineConfigNameForPools(pools) mcNames[mcName] = true } diff --git a/pkg/operator/mc.go b/pkg/operator/mc.go index 36fe9de2aa..fde490a79c 100644 --- a/pkg/operator/mc.go +++ b/pkg/operator/mc.go @@ -19,7 +19,7 @@ const ( MachineConfigPrefix string = "50-nto" ) -func newMachineConfig(name string, annotations map[string]string, labels map[string]string, kernelArguments []string) *mcfgv1.MachineConfig { +func NewMachineConfig(name string, annotations map[string]string, labels map[string]string, kernelArguments []string) *mcfgv1.MachineConfig { if labels == nil { labels = map[string]string{} } @@ -43,7 +43,7 @@ func newMachineConfig(name string, annotations map[string]string, labels map[str } } -func getMachineConfigNameForPools(pools []*mcfgv1.MachineConfigPool) string { +func GetMachineConfigNameForPools(pools []*mcfgv1.MachineConfigPool) string { var ( sb strings.Builder sbPrimary strings.Builder @@ -211,7 +211,7 @@ func (pc *ProfileCalculator) getPrimaryPoolForNode(node *corev1.Node) (*mcfgv1.M return pools[0], nil } -func machineConfigGenerationLogLine(bCmdline bool, bootcmdline string) string { +func MachineConfigGenerationLogLine(bCmdline bool, bootcmdline string) string { var ( sb strings.Builder ) diff --git a/pkg/operator/profilecalculator.go b/pkg/operator/profilecalculator.go index 3b64a95aa7..16be51c877 100644 --- a/pkg/operator/profilecalculator.go +++ b/pkg/operator/profilecalculator.go @@ -168,7 +168,7 @@ func (pc *ProfileCalculator) calculateProfile(nodeName string) (string, map[stri return "", nil, nil, operand, fmt.Errorf("failed to list Tuned: %v", err) } - for _, recommend := range tunedRecommend(tunedList) { + for _, recommend := range TunedRecommend(tunedList) { var ( pools []*mcfgv1.MachineConfigPool node *corev1.Node @@ -257,7 +257,7 @@ func (pc *ProfileCalculator) calculateProfileHyperShift(nodeName string) (string } tunedList = append(tunedList, defaultTuned) - for _, recommend := range tunedRecommend(tunedList) { + for _, recommend := range TunedRecommend(tunedList) { // Start with node/pod label based matching if recommend.Match != nil && pc.profileMatches(recommend.Match, nodeName) { klog.V(3).Infof("calculateProfileHyperShift: node / pod label matching used for node: %s, tunedProfileName: %s, nodePoolName: %s, operand: %v", nodeName, *recommend.Profile, "", recommend.Operand) @@ -512,7 +512,7 @@ func (pc *ProfileCalculator) tunedUsesPodLabels(match []tunedv1.TunedMatch) bool // tunedsUseNodeLabels returns true if any of the Tuned CRs uses Node labels. func (pc *ProfileCalculator) tunedsUseNodeLabels(tunedSlice []*tunedv1.Tuned) bool { - for _, recommend := range tunedRecommend(tunedSlice) { + for _, recommend := range TunedRecommend(tunedSlice) { if pc.tunedUsesNodeLabels(recommend.Match) { return true } @@ -522,7 +522,7 @@ func (pc *ProfileCalculator) tunedsUseNodeLabels(tunedSlice []*tunedv1.Tuned) bo // tunedsUsePodLabels returns true if any of the Tuned CRs uses Pod labels. func (pc *ProfileCalculator) tunedsUsePodLabels(tunedSlice []*tunedv1.Tuned) bool { - for _, recommend := range tunedRecommend(tunedSlice) { + for _, recommend := range TunedRecommend(tunedSlice) { if pc.tunedUsesPodLabels(recommend.Match) { return true } @@ -537,9 +537,9 @@ func (pc *ProfileCalculator) getNodePoolNameForNode(node *corev1.Node) (string, return nodePoolName, nil } -// tunedRecommend returns a priority-sorted TunedRecommend slice out of +// TunedRecommend returns a priority-sorted TunedRecommend slice out of // a slice of Tuned objects for profile-calculation purposes. -func tunedRecommend(tunedSlice []*tunedv1.Tuned) []tunedv1.TunedRecommend { +func TunedRecommend(tunedSlice []*tunedv1.Tuned) []tunedv1.TunedRecommend { var recommendAll []tunedv1.TunedRecommend // Tuned profiles should have unique priority across all Tuned CRs and users diff --git a/pkg/performanceprofile/cmd/render/render.go b/pkg/performanceprofile/cmd/render/render.go index f1113c5465..e19eb37040 100644 --- a/pkg/performanceprofile/cmd/render/render.go +++ b/pkg/performanceprofile/cmd/render/render.go @@ -31,6 +31,7 @@ import ( performancev2 "github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2" "github.com/openshift/cluster-node-tuning-operator/pkg/performanceprofile/controller/performanceprofile/components/machineconfig" "github.com/openshift/cluster-node-tuning-operator/pkg/performanceprofile/controller/performanceprofile/components/manifestset" + "github.com/openshift/cluster-node-tuning-operator/pkg/util" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -72,7 +73,7 @@ func render(inputDir, outputDir string) error { klog.Info("Rendering files into: ", outputDir) // Read asset directory fileInfo - filePaths, err := listFiles(inputDir) + filePaths, err := util.ListFiles(inputDir) klog.V(4).Infof("listed files: %v", filePaths) if err != nil { return err @@ -99,7 +100,7 @@ func render(inputDir, outputDir string) error { } defer file.Close() - manifests, err := parseManifests(file.Name(), file) + manifests, err := util.ParseManifests(file.Name(), file) if err != nil { return fmt.Errorf("error parsing manifests from %s: %w", file.Name(), err) } diff --git a/pkg/tuned/cmd/render/cmd.go b/pkg/tuned/cmd/render/cmd.go new file mode 100644 index 0000000000..51dbc8a108 --- /dev/null +++ b/pkg/tuned/cmd/render/cmd.go @@ -0,0 +1,85 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package render + +import ( + "flag" + "fmt" + + "github.com/openshift/cluster-node-tuning-operator/pkg/performanceprofile/controller/performanceprofile/components" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "k8s.io/klog" +) + +type renderOpts struct { + assetsInDir []string + assetsOutDir string +} + +func NewRenderBootCmdMCCommand() *cobra.Command { + renderOpts := renderOpts{} + + cmd := &cobra.Command{ + Use: "render-bootcmd-mc", + Short: "Render MC with kernel args", + Run: func(cmd *cobra.Command, args []string) { + if err := renderOpts.Validate(); err != nil { + klog.Fatal(err) + } + + if err := renderOpts.Run(); err != nil { + klog.Fatal(err) + } + }, + } + + addKlogFlags(cmd) + renderOpts.AddFlags(cmd.Flags()) + return cmd +} + +func (r *renderOpts) AddFlags(fs *pflag.FlagSet) { + fs.StringArrayVar(&r.assetsInDir, "asset-input-dir", []string{components.AssetsDir}, "Input path for the assets directory. (Can use it more than one to define multiple directories)") + fs.StringVar(&r.assetsOutDir, "asset-output-dir", r.assetsOutDir, "Output path for the rendered manifests.") +} + +func (r *renderOpts) Validate() error { + var err string + if len(r.assetsInDir) == 0 { + err += "asset-input-dir must be specified. " + } + if len(r.assetsOutDir) == 0 { + err += "asset-output-dir must be specified. " + } + + if len(err) == 0 { + return nil + } + return fmt.Errorf(err) +} + +func (r *renderOpts) Run() error { + return render(r.assetsInDir, r.assetsOutDir) +} + +func addKlogFlags(cmd *cobra.Command) { + fs := flag.NewFlagSet("", flag.PanicOnError) + klog.InitFlags(fs) + cmd.Flags().AddGoFlagSet(fs) +} diff --git a/pkg/tuned/cmd/render/render.go b/pkg/tuned/cmd/render/render.go new file mode 100644 index 0000000000..535ba74a2b --- /dev/null +++ b/pkg/tuned/cmd/render/render.go @@ -0,0 +1,264 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package render + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + performancev2 "github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2" + "github.com/openshift/cluster-node-tuning-operator/pkg/manifests" + "github.com/openshift/cluster-node-tuning-operator/pkg/operator" + "github.com/openshift/cluster-node-tuning-operator/pkg/performanceprofile/controller/performanceprofile/components/tuned" + "sigs.k8s.io/yaml" + + tunedv1 "github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned/v1" + tunedpkg "github.com/openshift/cluster-node-tuning-operator/pkg/tuned" + + "github.com/openshift/cluster-node-tuning-operator/pkg/util" + "github.com/openshift/cluster-node-tuning-operator/version" + mcfgv1 "github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/klog" +) + +var ( + manifestScheme = runtime.NewScheme() + codecFactory serializer.CodecFactory + runtimeDecoder runtime.Decoder +) + +func init() { + utilruntime.Must(performancev2.AddToScheme(manifestScheme)) + utilruntime.Must(mcfgv1.Install(manifestScheme)) + utilruntime.Must(tunedv1.AddToScheme(manifestScheme)) + codecFactory = serializer.NewCodecFactory(manifestScheme) + runtimeDecoder = codecFactory.UniversalDecoder( + performancev2.GroupVersion, + mcfgv1.GroupVersion, + tunedv1.SchemeGroupVersion, + ) +} + +func render(inputDir []string, outputDir string) error { + klog.Info("Rendering files from: ", inputDir) + klog.Info("Rendering files into: ", outputDir) + + bootstrapSafeEnv := os.Getenv("CLUSTER_NODE_TUNED_BOOTSTRAP_SAFE_ENV") + if len(bootstrapSafeEnv) == 0 { + return fmt.Errorf("Should only be run on bootstrap safe environment. Please define env var 'CLUSTER_NODE_TUNED_BOOTSTRAP_SAFE_ENV' ") + } + + // Get pools, mConfigs and profile from inputDir + // Read asset directory fileInfo + filePaths, err := util.ListFilesFromMultiplePaths(inputDir) + if err != nil { + return fmt.Errorf("error while listing files: %w", err) + } + klog.Infof("listed files: %v", filePaths) + // Make output dir if not present + err = os.MkdirAll(outputDir, os.ModePerm) + if err != nil { + return fmt.Errorf("Error while creating outputdir %s : %w", outputDir, err) + } + + var ( + perfProfiles []*performancev2.PerformanceProfile + mcPools []*mcfgv1.MachineConfigPool + mcConfigs []*mcfgv1.MachineConfig + tuneD []*tunedv1.Tuned + ) + + // Iterate through the file paths and read in desired files + klog.Info("Iterating over listed files ... ") + for _, path := range filePaths { + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("error opening %s: %w", file.Name(), err) + } + defer file.Close() + + manifests, err := util.ParseManifests(file.Name(), file) + if err != nil { + return fmt.Errorf("error parsing manifests from %s: %w", file.Name(), err) + } + + // Decode manifest files + klog.V(4).Infof("decoding manifests for file %s...", path) + for idx, m := range manifests { + obji, err := runtime.Decode(runtimeDecoder, m.Raw) + if err != nil { + if runtime.IsNotRegisteredError(err) { + klog.V(4).Infof("skipping path %q [%d] manifest because it is not part of expected api group: %v", file.Name(), idx+1, err) + continue + } + return fmt.Errorf("error parsing %q [%d] manifest: %w", file.Name(), idx+1, err) + } + + switch obj := obji.(type) { + case *performancev2.PerformanceProfile: + perfProfiles = append(perfProfiles, obj) + case *mcfgv1.MachineConfigPool: + mcPools = append(mcPools, obj) + case *mcfgv1.MachineConfig: + mcConfigs = append(mcConfigs, obj) + case *tunedv1.Tuned: + tuneD = append(tuneD, obj) + default: + klog.Infof("skipping %q [%d] manifest because of unhandled %T", file.Name(), idx+1, obji) + } + } + } + + perfProfilesTuneDProfiles := make(map[*performancev2.PerformanceProfile]*tunedv1.Tuned, len(perfProfiles)) + for _, profile := range perfProfiles { + tunedFromPP, err := tuned.NewNodePerformance(profile) + if err != nil { + klog.Errorf("Unable to get tuned. error : %v", err) + return fmt.Errorf("unable to get tuned from profile: %w", err) + } + perfProfilesTuneDProfiles[profile] = tunedFromPP + // add tuned from PP to the list + tuneD = append(tuneD, tunedFromPP) + } + + tuneDrecommended := operator.TunedRecommend(tuneD) + if len(tuneDrecommended) == 0 { + klog.Error("Unable to get tuned recommended profile.") + return fmt.Errorf("Unable to get tuned recommended profile.") + } + + recommendedProfile := *tuneDrecommended[0].Profile + err = tunedpkg.TunedRecommendFileWrite(recommendedProfile) + if err != nil { + klog.Errorf("error writing recommended profile %q : %v", recommendedProfile, err) + return fmt.Errorf("error writing recommended profile %q : %w", recommendedProfile, err) + } + + t := manifests.TunedRenderedResource(tuneD) + //extract all the profiles. + _, _, _, err = tunedpkg.ProfilesExtract(t.Spec.Profile, recommendedProfile) + if err != nil { + klog.Errorf("error extracting tuned profiles : %v", err) + return fmt.Errorf("error extracting tuned profiles: %w", err) + } + + //Should run tuned + tunedCmd := tunedpkg.TunedCreateCmd(false) + err = tunedpkg.TunedRunNoDaemon(tunedCmd) + if err != nil { + klog.Errorf("Unable to run tuned error : %v", err) + return err + } + + bootcmdline, err := tunedpkg.GetBootcmdline() + if err != nil { + klog.Errorf("Unable to get bootcmdline. error : %v", err) + return err + } + + for profile, tunedFromPP := range perfProfilesTuneDProfiles { + mc, err := renderMachineConfig(mcPools, bootcmdline, mcConfigs, profile, tunedFromPP) + if err != nil { + klog.Errorf("error while rendering machine config %v", err) + return fmt.Errorf("error while rendering machine config: %w", err) + } + + if mc != nil { + //Render mc in output dir + byteOutput, err := yaml.Marshal(mc) + if err != nil { + klog.Errorf("Unable to render output machineconfig. error : %v", err) + return err + } + + fileName := fmt.Sprintf("%s_%s_kargs.yaml", profile.Name, strings.ToLower(mc.Kind)) + fullFilePath := filepath.Join(outputDir, fileName) + klog.Info("Writing file: ", fullFilePath) + err = os.WriteFile(fullFilePath, byteOutput, 0644) + if err != nil { + klog.Errorf("Unable to write output file %s. error : %v", fullFilePath, err) + return err + } + + klog.Infof("MachineConfig written at : %s", fullFilePath) + } + } + return nil +} + +func renderMachineConfig(pools []*mcfgv1.MachineConfigPool, bootcmdline string, mConfigs []*mcfgv1.MachineConfig, profile *performancev2.PerformanceProfile, tunedMf *tunedv1.Tuned) (*mcfgv1.MachineConfig, error) { + if len(bootcmdline) == 0 { + klog.Info("Empty cmdbootline. Avoid creating MachineConfig") + return nil, nil + } + + mcName := operator.GetMachineConfigNameForPools(pools) + kernelArgs := util.SplitKernelArguments(bootcmdline) + annotations := map[string]string{operator.GeneratedByControllerVersionAnnotationKey: version.Version} + labels := tunedMf.Spec.Recommend[0].MachineConfigLabels + + mc := getMachineConfigByName(mConfigs, mcName) + if mc == nil { //not found + // Expect only one PerformanceProfile => one TuneD + mc = operator.NewMachineConfig(mcName, annotations, labels, kernelArgs) + klog.Infof("rendered MachineConfig %s with%s", mc.ObjectMeta.Name, operator.MachineConfigGenerationLogLine(len(bootcmdline) != 0, bootcmdline)) + return mc, nil + } + + // found a MC need to modify it + mcNew := operator.NewMachineConfig(mcName, annotations, labels, kernelArgs) + + kernelArgsEq := util.StringSlicesEqual(mc.Spec.KernelArguments, kernelArgs) + if kernelArgsEq { + // No update needed + klog.Infof("renderMachineConfig: MachineConfig %s doesn't need updating", mc.ObjectMeta.Name) + return nil, nil + } + + mc = mc.DeepCopy() // never update the objects from cache + mc.ObjectMeta.Annotations = mcNew.ObjectMeta.Annotations + mc.Spec.KernelArguments = removeDuplicates(append(mc.Spec.KernelArguments, kernelArgs...)) + mc.Spec.Config = mcNew.Spec.Config + l := operator.MachineConfigGenerationLogLine(!kernelArgsEq, bootcmdline) + klog.Infof("renderMachineConfig: updating MachineConfig %s with%s", mc.ObjectMeta.Name, l) + + return mc, nil +} + +func getMachineConfigByName(mConfigs []*mcfgv1.MachineConfig, name string) *mcfgv1.MachineConfig { + for _, mc := range mConfigs { + if mc.Name == name { + return mc + } + } + return nil +} + +func removeDuplicates[T string | int](sliceList []T) []T { + allKeys := make(map[T]bool) + list := []T{} + for _, item := range sliceList { + if _, value := allKeys[item]; !value { + allKeys[item] = true + list = append(list, item) + } + } + return list +} diff --git a/pkg/tuned/controller.go b/pkg/tuned/controller.go index 650939e424..e3a4b7573c 100644 --- a/pkg/tuned/controller.go +++ b/pkg/tuned/controller.go @@ -90,6 +90,24 @@ const ( // Types type Bits uint8 +type Daemon struct { + // reloading is true during the TuneD daemon reload. + reloading bool + // reloaded is true immediately after the TuneD daemon finished reloading. + // and the node Profile k8s object's Status needs to be set for the operator; + // it is set to false on successful Profile update. + reloaded bool + // debugging flag + debug bool + // bit/set representaton of Profile status conditions to report back via API. + status Bits + // stderr log from TuneD daemon to report back via API. + stderr string + // stopping is true while the controller tries to stop the TuneD daemon. + stopping bool + // the TuneD profile we wish to be applied. + recommendedProfile string +} type Controller struct { kubeconfig *restclient.Config kubeclient kubernetes.Interface @@ -115,24 +133,7 @@ type Controller struct { daemon bool } - daemon struct { - // reloading is true during the TuneD daemon reload. - reloading bool - // reloaded is true immediately after the TuneD daemon finished reloading. - // and the node Profile k8s object's Status needs to be set for the operator; - // it is set to false on successful Profile update. - reloaded bool - // debugging flag - debug bool - // bit/set representaton of Profile status conditions to report back via API. - status Bits - // stderr log from TuneD daemon to report back via API. - stderr string - // stopping is true while the controller tries to stop the TuneD daemon. - stopping bool - // the TuneD profile we wish to be applied. - recommendedProfile string - } + daemon Daemon tunedCmd *exec.Cmd // external command (tuned) being prepared or run tunedExit chan bool // bi-directional channel to signal and register TuneD daemon exit @@ -305,7 +306,7 @@ func (c *Controller) sync(key wqKey) error { } c.daemon.recommendedProfile = profile.Spec.Config.TunedProfile - err = tunedRecommendFileWrite(c.daemon.recommendedProfile) + err = TunedRecommendFileWrite(c.daemon.recommendedProfile) if err != nil { return err } @@ -363,18 +364,18 @@ func profilesEqual(profileFile string, profileData string) bool { return profileData == string(content) } -// profilesExtract extracts TuneD daemon profiles to the daemon configuration directory. +// ProfilesExtract extracts TuneD daemon profiles to the daemon configuration directory. // Returns: // - True if the data in the to-be-extracted recommended profile or the profiles being // included from the current recommended profile have changed. // - A map with successfully extracted TuneD profile names. // - A map with names of TuneD profiles the current TuneD recommended profile depends on. // - Error if any or nil. -func profilesExtract(profiles []tunedv1.TunedProfile, recommendedProfile string) (bool, map[string]bool, map[string]bool, error) { +func ProfilesExtract(profiles []tunedv1.TunedProfile, recommendedProfile string) (bool, map[string]bool, map[string]bool, error) { var ( change bool ) - klog.Infof("extracting TuneD profiles") + klog.Info("profilesExtract(): extracting TuneD profiles") // Get a list of TuneD profiles names the recommended profile depends on. recommendedProfileDeps := profileDepends(recommendedProfile) @@ -431,7 +432,7 @@ func profilesExtract(profiles []tunedv1.TunedProfile, recommendedProfile string) // included from the current recommended profile have changed. // - Error if any or nil. func profilesSync(profiles []tunedv1.TunedProfile, recommendedProfile string) (bool, error) { - change, extractedNew, recommendedProfileDeps, err := profilesExtract(profiles, recommendedProfile) + change, extractedNew, recommendedProfileDeps, err := ProfilesExtract(profiles, recommendedProfile) if err != nil { return change, err } @@ -492,7 +493,7 @@ func openshiftTunedPidFileWrite() error { return nil } -func tunedRecommendFileWrite(profileName string) error { +func TunedRecommendFileWrite(profileName string) error { klog.V(2).Infof("tunedRecommendFileWrite(): %s", profileName) if err := util.Mkdir(tunedRecommendDir); err != nil { return fmt.Errorf("failed to create directory %q: %v", tunedRecommendDir, err) @@ -534,11 +535,7 @@ func overridenSysctl(data string) string { } func (c *Controller) tunedCreateCmd() *exec.Cmd { - args := []string{"--no-dbus"} - if c.daemon.debug { - args = append(args, "--debug") - } - return exec.Command("/usr/sbin/tuned", args...) + return TunedCreateCmd(c.daemon.debug) } func (c *Controller) tunedRun() { @@ -548,85 +545,19 @@ func (c *Controller) tunedRun() { close(c.tunedExit) }() - cmdReader, err := c.tunedCmd.StderrPipe() - if err != nil { - klog.Errorf("error creating StderrPipe for tuned: %v", err) - return - } - - scanner := bufio.NewScanner(cmdReader) - go func() { - for scanner.Scan() { - l := scanner.Text() - - fmt.Printf("%s\n", l) - - if c.daemon.stopping { - // We have decided to stop TuneD. Apart from showing the logs it is - // now unnecessary/undesirable to perform any of the following actions. - // The undesirability comes from extra processing which will come if - // TuneD manages to "get unstuck" during this phase before it receives - // SIGKILL (note the time window between SIGTERM/SIGKILL). - continue - } - - profileApplied := strings.Contains(l, " tuned.daemon.daemon: static tuning from profile ") && strings.Contains(l, " applied") - reloadFailed := strings.Contains(l, " tuned.daemon.controller: Failed to reload TuneD: ") - - if profileApplied { - c.daemon.status |= scApplied - } - - strIndex := strings.Index(l, " WARNING ") - if strIndex >= 0 { - c.daemon.status |= scWarn - prevError := ((c.daemon.status & scError) != 0) - if !prevError { // don't overwrite an error message - c.daemon.stderr = l[strIndex:] // trim timestamp from log - } - } - - strIndex = strings.Index(l, " ERROR ") - if strIndex >= 0 { - c.daemon.status |= scError - c.daemon.stderr = l[strIndex:] // trim timestamp from log - } - - sysctl := overridenSysctl(l) - if sysctl != "" { - c.daemon.status |= scSysctlOverride - c.daemon.stderr = sysctl - } + onDaemonReload := func() { + klog.V(2).Infof("profile applied or reload failed, stopping the TuneD watcher") + c.tunedTimeout = tunedInitialTimeout // initialize the timeout + c.daemon.status &= ^scTimeout // clear the scTimeout status bit + c.tunedTicker.Stop() // profile applied or reload failed, stop the TuneD watcher - if c.daemon.reloading { - c.daemon.reloading = !profileApplied && !reloadFailed - c.daemon.reloaded = !c.daemon.reloading - if c.daemon.reloaded { - klog.V(2).Infof("profile applied or reload failed, stopping the TuneD watcher") - c.tunedTimeout = tunedInitialTimeout // initialize the timeout - c.daemon.status &= ^scTimeout // clear the scTimeout status bit - c.tunedTicker.Stop() // profile applied or reload failed, stop the TuneD watcher - - // Notify the event processor that the TuneD daemon finished reloading. - c.wqTuneD.Add(wqKey{kind: wqKindDaemon}) - } - } - } - }() - - c.daemon.reloading = true - // Clear the set out of which Profile status conditions are created. Keep timeout condition if already set. - c.daemon.status &= scTimeout - c.daemon.stderr = "" - if err = c.tunedCmd.Start(); err != nil { - klog.Errorf("error starting tuned: %v", err) - return + // Notify the event processor that the TuneD daemon finished reloading. + c.wqTuneD.Add(wqKey{kind: wqKindDaemon}) } - if err = c.tunedCmd.Wait(); err != nil { - // The command exited with non 0 exit status, e.g. terminated by a signal. - klog.Errorf("error waiting for tuned: %v", err) - return + err := TunedRun(c.tunedCmd, &c.daemon, onDaemonReload) + if err != nil { + klog.Errorf("Error while running tuned %v", err) } } @@ -746,7 +677,7 @@ func getActiveProfile() (string, error) { return responseString, nil } -func getBootcmdline() (string, error) { +func GetBootcmdline() (string, error) { var responseString = "" f, err := os.Open(tunedBootcmdlineFile) @@ -956,7 +887,7 @@ func (c *Controller) updateTunedProfile() (err error) { bootcmdline string ) - if bootcmdline, err = getBootcmdline(); err != nil { + if bootcmdline, err = GetBootcmdline(); err != nil { // This should never happen unless something is seriously wrong (e.g. TuneD // daemon no longer uses tunedBootcmdlineFile). Do not continue. return fmt.Errorf("unable to get kernel command-line parameters: %v", err) diff --git a/pkg/tuned/run.go b/pkg/tuned/run.go new file mode 100644 index 0000000000..9b52bc2447 --- /dev/null +++ b/pkg/tuned/run.go @@ -0,0 +1,169 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tuned + +import ( + "bufio" + "fmt" + "os/exec" + "strings" + + "k8s.io/klog/v2" +) + +func TunedCreateCmd(debug bool) *exec.Cmd { + args := []string{"--no-dbus"} + if debug { + args = append(args, "--debug") + } + return exec.Command("/usr/sbin/tuned", args...) +} + +func configDaemonMode() (func(), error) { + tunedMainCfgFilename := tunedProfilesDirCustom + "/" + tunedMainConfFile + daemon_key := "daemon" + + tunedMainCfg, err := iniFileLoad(tunedMainCfgFilename) + if err != nil { + return nil, fmt.Errorf("failed to read global TuneD configuration file: %w", err) + } + + daemon_value := tunedMainCfg.Section("").Key(daemon_key).MustBool() + + err = iniCfgSetKey(tunedMainCfg, daemon_key, false) + if err != nil { + return nil, err + } + err = iniFileSave(tunedMainCfgFilename, tunedMainCfg) + if err != nil { + return nil, fmt.Errorf("failed to write global TuneD configuration file: %w", err) + } + + restoreF := func() { + tunedMainCfg, err := iniFileLoad(tunedMainCfgFilename) + if err != nil { + klog.Warningf("failed to read global TuneD configuration file: %v", err) + return + } + err = iniCfgSetKey(tunedMainCfg, daemon_key, daemon_value) + if err != nil { + klog.Warningf("failed to set %s key to %v value: %v", daemon_key, daemon_value, err) + return + } + err = iniFileSave(tunedMainCfgFilename, tunedMainCfg) + if err != nil { + klog.Warningf("failed to write global TuneD configuration file: %w", err) + } + } + + return restoreF, nil +} + +func TunedRunNoDaemon(cmd *exec.Cmd) error { + var daemon Daemon + + restoreFunction, err := configDaemonMode() + if err != nil { + return err + } + defer restoreFunction() + + onDaemonReload := func() {} + return TunedRun(cmd, &daemon, onDaemonReload) +} +func TunedRun(cmd *exec.Cmd, daemon *Daemon, onDaemonReload func()) error { + klog.Infof("running cmd...") + + cmdReader, err := cmd.StderrPipe() + if err != nil { + return fmt.Errorf("error creating StderrPipe for tuned: %w", err) + } + + scanner := bufio.NewScanner(cmdReader) + go func() { + for scanner.Scan() { + l := scanner.Text() + + fmt.Printf("%s\n", l) + + if daemon.stopping { + // We have decided to stop TuneD. Apart from showing the logs it is + // now unnecessary/undesirable to perform any of the following actions. + // The undesirability comes from extra processing which will come if + // TuneD manages to "get unstuck" during this phase before it receives + // SIGKILL (note the time window between SIGTERM/SIGKILL). + continue + } + + profileApplied := strings.Contains(l, " tuned.daemon.daemon: static tuning from profile ") && strings.Contains(l, " applied") + reloadFailed := strings.Contains(l, " tuned.daemon.controller: Failed to reload TuneD: ") + + if profileApplied { + daemon.status |= scApplied + } + + strIndex := strings.Index(l, " WARNING ") + if strIndex >= 0 { + daemon.status |= scWarn + prevError := ((daemon.status & scError) != 0) + if !prevError { // don't overwrite an error message + daemon.stderr = l[strIndex:] // trim timestamp from log + } + } + + strIndex = strings.Index(l, " ERROR ") + if strIndex >= 0 { + daemon.status |= scError + daemon.stderr = l[strIndex:] // trim timestamp from log + } + + sysctl := overridenSysctl(l) + if sysctl != "" { + daemon.status |= scSysctlOverride + daemon.stderr = sysctl + } + + if daemon.reloading { + daemon.reloading = !profileApplied && !reloadFailed + daemon.reloaded = !daemon.reloading + if daemon.reloaded { + klog.V(2).Infof("profile applied or reload failed, stopping the TuneD watcher") + onDaemonReload() + // c.tunedTimeout = tunedInitialTimeout // initialize the timeout + // c.daemon.status &= ^scTimeout // clear the scTimeout status bit + // c.tunedTicker.Stop() // profile applied or reload failed, stop the TuneD watcher + + // // Notify the event processor that the TuneD daemon finished reloading. + // c.wqTuneD.Add(wqKey{kind: wqKindDaemon}) + } + } + } + }() + + daemon.reloading = true + // Clear the set out of which Profile status conditions are created. Keep timeout condition if already set. + daemon.status &= scTimeout + daemon.stderr = "" + if err = cmd.Start(); err != nil { + return fmt.Errorf("error starting tuned: %w", err) + } + + if err = cmd.Wait(); err != nil { + // The command exited with non 0 exit status, e.g. terminated by a signal. + return fmt.Errorf("error waiting for tuned: %w", err) + } + + return nil +} diff --git a/pkg/performanceprofile/cmd/render/manifests.go b/pkg/util/manifests.go similarity index 85% rename from pkg/performanceprofile/cmd/render/manifests.go rename to pkg/util/manifests.go index cf266ec521..b923bb5a29 100644 --- a/pkg/performanceprofile/cmd/render/manifests.go +++ b/pkg/util/manifests.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package render +package util import ( "bytes" @@ -51,9 +51,9 @@ func (m *manifest) UnmarshalJSON(in []byte) error { return nil } -// parseManifests parses a YAML or JSON document that may contain one or more +// ParseManifests parses a YAML or JSON document that may contain one or more // kubernetes resources. -func parseManifests(filename string, r io.Reader) ([]manifest, error) { +func ParseManifests(filename string, r io.Reader) ([]manifest, error) { d := yamlutil.NewYAMLOrJSONDecoder(r, 1024) var manifests []manifest for { @@ -72,10 +72,14 @@ func parseManifests(filename string, r io.Reader) ([]manifest, error) { } } -func listFiles(dirPaths string) ([]string, error) { +func ListFiles(dirPaths string) ([]string, error) { dirs := strings.Split(dirPaths, ",") + return ListFilesFromMultiplePaths(dirs) +} + +func ListFilesFromMultiplePaths(dirPaths []string) ([]string, error) { results := []string{} - for _, dir := range dirs { + for _, dir := range dirPaths { err := filepath.WalkDir(dir, func(path string, info os.DirEntry, err error) error { if err != nil {