diff --git a/cmd/day1-utility/main.go b/cmd/day1-utility/main.go index c1b305561..4117be51b 100644 --- a/cmd/day1-utility/main.go +++ b/cmd/day1-utility/main.go @@ -21,6 +21,7 @@ func customUsage() { fmt.Fprintln(flag.CommandLine.Output(), "IMAGE_NAME: container image that contains kernel module .ko file") fmt.Fprintln(flag.CommandLine.Output(), "KMOD_NAME: kernel module name, that should be loaded (oot)") fmt.Fprintln(flag.CommandLine.Output(), "IN_TREE_KMOD_TO_REMOVE: in-tree kernel module name, that should be unloaded.If no need - pass empty string") + fmt.Fprintln(flag.CommandLine.Output(), "FIRMWARE_FILES_PATH: path of the firmware files.Passing empty string means there are no files") fmt.Fprintln(flag.CommandLine.Output(), "WORKER_IMAGE: kernel-management worker image to use.Passing empty string means using default image") } @@ -32,6 +33,7 @@ func main() { kernelModule string workerImage string inTreeKernelModule string + firmwareFilesPath string ) flag.StringVar(&mcName, "machine-config", "", "name of the machine config to create") flag.StringVar(&mpName, "machine-config-pool", "", "name of the machine config pool to use") @@ -39,11 +41,12 @@ func main() { flag.StringVar(&kernelModule, "kernel-module", "", "container image that contains kernel module .ko file") flag.StringVar(&workerImage, "worker-image", "", "kernel-management worker image to use. If not passed, a default value will be used") flag.StringVar(&inTreeKernelModule, "in-tree-module-to-remove", "", "in-tree kernel module that should be removed prior to loading the oot module") + flag.StringVar(&firmwareFilesPath, "firmware-files-path", "", "path where the firmware files are located at") flag.Parse() flag.Usage = customUsage - yaml, err := mcproducer.ProduceMachineConfig(mcName, mpName, kernelModuleImage, kernelModule, inTreeKernelModule, workerImage) + yaml, err := mcproducer.ProduceMachineConfig(mcName, mpName, kernelModuleImage, kernelModule, inTreeKernelModule, firmwareFilesPath, workerImage) if err != nil { fmt.Println("failed to produce MachineConfig yaml", "error", err, "git commit", GitCommit, "version", Version) os.Exit(1) diff --git a/docs/mkdocs/documentation/day1_limited_option.md b/docs/mkdocs/documentation/day1_limited_option.md index 48878c6cf..7127b6761 100644 --- a/docs/mkdocs/documentation/day1_limited_option.md +++ b/docs/mkdocs/documentation/day1_limited_option.md @@ -70,6 +70,8 @@ The parameters are: module - `workerImage`: optional parameter. The worker image to use. In case this parameter is not passed, the default worker image will be used: quay.io/edge-infrastructure/kernel-module-management-worker:latest. + - `firmwareFilesPath`:` optional parameter. In case there is a need to also use firmware, + this parameter should hold the path to the directory containing those files as a string format. The API is located under `pkg/mcproducer` package of the KMM source code. @@ -78,17 +80,18 @@ Users only need to import the `pkg/mcproducer` package into their operator/utili MCO YAML to the cluster. ### Utility -`day1-utility` can be called from a shell. day1-utility executable is not a part of KMM github repo. +`day1-utility` can be called from a shell. day1-utility executable is not a part of KMM GitHub repo. In order to build it the following commands needs to be run: `make day1-utility` Utility uses the following flags: -`-image `: container image that contains kernel module .ko file -`-kernel-module `: name of the OOT module to load -`-machine-config `: name of the machine config to create -`-machine-config-pool `: name of the machine config pool to use +`-image `: container image that contains kernel module .ko file. +`-kernel-module `: name of the OOT module to load. +`-machine-config `: name of the machine config to create. +`-machine-config-pool `: name of the machine config pool to use. `-in-tree-module-to-remove `: in-tree kernel module that should be removed prior to loading the oot module. -`-worker-image `: kernel-management worker image to use. If not passed, a default value will be used +`-worker-image `: kernel-management worker image to use. If not passed, a default value will be used. +`-firmware-files-path `: path to the firmware files inside the module image. The first 4 flags are mandatory, but the last 2 are optional. They correspond to the parameters of the API diff --git a/pkg/mcproducer/mcproducer.go b/pkg/mcproducer/mcproducer.go index 42b29026a..84493951f 100644 --- a/pkg/mcproducer/mcproducer.go +++ b/pkg/mcproducer/mcproducer.go @@ -39,6 +39,7 @@ func ProduceMachineConfig(machineConfigName, kernelModuleImage, kernelModuleName, inTreeModuleToRemove, + firmwareFilesPath, workerImage string) (string, error) { err := verifyKernelModuleImage(kernelModuleImage) @@ -52,6 +53,7 @@ func ProduceMachineConfig(machineConfigName, } templateParams := map[string]any{ + "FirmwareFilesPath": firmwareFilesPath, "KernelModuleImage": kernelModuleImage, "KernelModule": kernelModuleName, "MachineConfigPoolRef": machineConfigPoolRef, diff --git a/pkg/mcproducer/mcproducer_test.go b/pkg/mcproducer/mcproducer_test.go index d619cc0f6..1bc8b5d00 100644 --- a/pkg/mcproducer/mcproducer_test.go +++ b/pkg/mcproducer/mcproducer_test.go @@ -13,12 +13,13 @@ var _ = Describe("ProduceMachineConfig", func() { mcpRef = "mcpRef" kernelModuleName = "testKernelModuleName" inTreeKernelModuleName = "testInTreeKernelModuleName" + firmwareFilesPath = "/opt/lib/test/firmware" ) It("image name format is invalid", func() { imageName := "quay.io/project/repo@sha2561f5f1ae25db67aa82707e1b1dc96c8a53ef7094f320b7eeaef12be9a13fa251d" - res, err := ProduceMachineConfig(name, mcpRef, imageName, kernelModuleName, "", "") + res, err := ProduceMachineConfig(name, mcpRef, imageName, kernelModuleName, "", firmwareFilesPath, "") Expect(err).To(HaveOccurred()) Expect(res).To(Equal("")) @@ -32,7 +33,7 @@ var _ = Describe("ProduceMachineConfig", func() { It("verify correct mco output", func() { imageName := "quay.io/project/repo:some-tag12" - res, err := ProduceMachineConfig(name, mcpRef, imageName, kernelModuleName, inTreeKernelModuleName, "") + res, err := ProduceMachineConfig(name, mcpRef, imageName, kernelModuleName, inTreeKernelModuleName, firmwareFilesPath, "") Expect(err).ToNot(HaveOccurred()) expectedRes, err := os.ReadFile("testdata/machineconfig-test.yaml") diff --git a/pkg/mcproducer/scripts/replace-kernel-module.sh b/pkg/mcproducer/scripts/replace-kernel-module.sh index 4a5c35039..2ba52418b 100644 --- a/pkg/mcproducer/scripts/replace-kernel-module.sh +++ b/pkg/mcproducer/scripts/replace-kernel-module.sh @@ -5,6 +5,7 @@ in_tree_module_to_remove="$IN_TREE_MODULE_TO_REMOVE" kernel_module="$KERNEL_MODULE" worker_image="$WORKER_IMAGE" kernel_module_image="$KERNEL_MODULE_IMAGE" +firmware_files_path="$FIRMWARE_FILES_PATH" kernel_module_image_tag=$(uname -r) full_kernel_module_image="$kernel_module_image:$kernel_module_image_tag" worker_pod_name=kmm-pod @@ -31,14 +32,20 @@ if [ -n "$(podman images -q $full_kernel_module_image 2> /dev/null)" ]; then podman volume create $worker_volume_name podman pod create --name $worker_pod_name echo "creating init container" + copycmd="mkdir -p /tmp/opt/lib/modules && cp -R /opt/lib/modules/* /tmp/opt/lib/modules;" + if [[ -n "$FIRMWARE_FILES_PATH" ]]; then + folders=("tmp" "$firmware_files_path"); + path_to_copy_firmware=$(printf '/%s' "${folders[@]%/}") + copycmd+=" mkdir -p ${path_to_copy_firmware} && \ + cp -R ${firmware_files_path}/* ${path_to_copy_firmware}" + fi podman create \ - --pod $worker_pod_name \ - --init-ctr=always \ - --rm \ - -v $worker_volume_name:/tmp \ - $full_kernel_module_image \ - /bin/sh -c "mkdir -p /tmp/opt/lib/modules && \ - cp -R /opt/lib/modules/* /tmp/opt/lib/modules" + --pod $worker_pod_name \ + --init-ctr=always \ + --rm \ + -v $worker_volume_name:/tmp \ + $full_kernel_module_image \ + /bin/sh -c "${copycmd}" echo "creating worker container" worker_pod_id=$( podman create \ @@ -59,6 +66,8 @@ if [ -n "$(podman images -q $full_kernel_module_image 2> /dev/null)" ]; then echo "failed to insert OOT kernel module $kernel_module" fi podman wait $worker_pod_id + echo "removing kmm-pod" + podman pod rm $worker_pod_name echo "removing volume" podman volume rm $worker_volume_name else diff --git a/pkg/mcproducer/templates/machine-config.gotmpl b/pkg/mcproducer/templates/machine-config.gotmpl index 3439670b5..6f34096d4 100644 --- a/pkg/mcproducer/templates/machine-config.gotmpl +++ b/pkg/mcproducer/templates/machine-config.gotmpl @@ -21,6 +21,7 @@ spec: Type=oneshot TimeoutSec=10 EnvironmentFile=/etc/mco/proxy.env + Environment="FIRMWARE_FILES_PATH={{.FirmwareFilesPath}}" Environment="IN_TREE_MODULE_TO_REMOVE={{.InTreeModuleToRemove}}" Environment="KERNEL_MODULE={{.KernelModule}}" Environment="KERNEL_MODULE_IMAGE={{.KernelModuleImage}}" diff --git a/pkg/mcproducer/testdata/machineconfig-test.yaml b/pkg/mcproducer/testdata/machineconfig-test.yaml index ad3280314..e2fb2ea65 100644 --- a/pkg/mcproducer/testdata/machineconfig-test.yaml +++ b/pkg/mcproducer/testdata/machineconfig-test.yaml @@ -21,6 +21,7 @@ spec: Type=oneshot TimeoutSec=10 EnvironmentFile=/etc/mco/proxy.env + Environment="FIRMWARE_FILES_PATH=/opt/lib/test/firmware" Environment="IN_TREE_MODULE_TO_REMOVE=testInTreeKernelModuleName" Environment="KERNEL_MODULE=testKernelModuleName" Environment="KERNEL_MODULE_IMAGE=quay.io/project/repo:some-tag12" @@ -68,7 +69,7 @@ spec: user: name: "root" contents: - source: "data:text/plain;base64,IyEvYmluL2Jhc2gKCmttbV9jb25maWdfZmlsZV9maWxlcGF0aD0iJFdPUktFUl9DT05GSUdfRklMRVBBVEgiCmluX3RyZWVfbW9kdWxlX3RvX3JlbW92ZT0iJElOX1RSRUVfTU9EVUxFX1RPX1JFTU9WRSIKa2VybmVsX21vZHVsZT0iJEtFUk5FTF9NT0RVTEUiCndvcmtlcl9pbWFnZT0iJFdPUktFUl9JTUFHRSIKa2VybmVsX21vZHVsZV9pbWFnZT0iJEtFUk5FTF9NT0RVTEVfSU1BR0UiCmtlcm5lbF9tb2R1bGVfaW1hZ2VfdGFnPSQodW5hbWUgLXIpCmZ1bGxfa2VybmVsX21vZHVsZV9pbWFnZT0iJGtlcm5lbF9tb2R1bGVfaW1hZ2U6JGtlcm5lbF9tb2R1bGVfaW1hZ2VfdGFnIgp3b3JrZXJfcG9kX25hbWU9a21tLXBvZAp3b3JrZXJfdm9sdW1lX25hbWU9a21tLXZvbHVtZQoKY3JlYXRlX2ttbV9jb25maWcoKSB7CiAgICAjIFdyaXRlIFlBTUwgY29udGVudCB0byB0aGUgZmlsZQogICAgY2F0IDw8RU9GID4gIiRrbW1fY29uZmlnX2ZpbGVfZmlsZXBhdGgiCmNvbnRhaW5lckltYWdlOiAkZnVsbF9rZXJuZWxfbW9kdWxlX2ltYWdlCmluVHJlZU1vZHVsZVRvUmVtb3ZlOiAkaW5fdHJlZV9tb2R1bGVfdG9fcmVtb3ZlCm1vZHByb2JlOgogIGRpck5hbWU6IC9vcHQKICBtb2R1bGVOYW1lOiAka2VybmVsX21vZHVsZQpFT0YKICAgIGVjaG8gImxvZ2dpbmcgY29udGVudHMgb2YgdGhlIHdvcmtlciBjb25maWcgZmlsZToiCiAgICBjYXQgIiRrbW1fY29uZmlnX2ZpbGVfZmlsZXBhdGgiCn0KCmVjaG8gImJlZm9yZSBjaGVja2luZyBpbWFnZSBwcmVzZW5jZSIKaWYgWyAtbiAiJChwb2RtYW4gaW1hZ2VzIC1xICRmdWxsX2tlcm5lbF9tb2R1bGVfaW1hZ2UgMj4gL2Rldi9udWxsKSIgXTsgdGhlbgogICAgZWNobyAiSW1hZ2UgJGZ1bGxfa2VybmVsX21vZHVsZV9pbWFnZSBmb3VuZCBvbiB0aGUgbG9jYWwgZmlsZSBzeXN0ZW0sIGNyZWF0aW5nIGttbSBjb25maWcgZmlsZSIKICAgIGNyZWF0ZV9rbW1fY29uZmlnCiAgICBlY2hvICJjcmVhdGluZyB2b2x1bWUiCiAgICBwb2RtYW4gdm9sdW1lIGNyZWF0ZSAkd29ya2VyX3ZvbHVtZV9uYW1lCiAgICBwb2RtYW4gcG9kIGNyZWF0ZSAtLW5hbWUgJHdvcmtlcl9wb2RfbmFtZQogICAgZWNobyAiY3JlYXRpbmcgaW5pdCBjb250YWluZXIiCiAgICBwb2RtYW4gY3JlYXRlIFwKICAgICAgLS1wb2QgJHdvcmtlcl9wb2RfbmFtZSBcCiAgICAgIC0taW5pdC1jdHI9YWx3YXlzIFwKICAgICAgLS1ybSBcCiAgICAgIC12ICR3b3JrZXJfdm9sdW1lX25hbWU6L3RtcCBcCiAgICAgICRmdWxsX2tlcm5lbF9tb2R1bGVfaW1hZ2UgXAogICAgICAvYmluL3NoIC1jICJta2RpciAtcCAvdG1wL29wdC9saWIvbW9kdWxlcyAmJiBcCiAgICAgIGNwIC1SIC9vcHQvbGliL21vZHVsZXMvKiAvdG1wL29wdC9saWIvbW9kdWxlcyIKICAgIGVjaG8gImNyZWF0aW5nIHdvcmtlciBjb250YWluZXIiCiAgICB3b3JrZXJfcG9kX2lkPSQoCiAgICBwb2RtYW4gY3JlYXRlIFwKICAgICAgLS1wb2QgJHdvcmtlcl9wb2RfbmFtZVwKICAgICAgLS11c2VyPXJvb3QgXAogICAgICAtLXByaXZpbGVnZWQgXAogICAgICAtLXJtIFwKICAgICAgLXYgJHdvcmtlcl92b2x1bWVfbmFtZTovdG1wIFwKICAgICAgLXYgL2xpYi9tb2R1bGVzOi9saWIvbW9kdWxlcyBcCiAgICAgIC12ICRrbW1fY29uZmlnX2ZpbGVfZmlsZXBhdGg6L2V0Yy9rbW0td29ya2VyL2NvbmZpZy55YW1sIFwKICAgICAgJHdvcmtlcl9pbWFnZSBcCiAgICAgIGttb2QgbG9hZCAvZXRjL2ttbS13b3JrZXIvY29uZmlnLnlhbWwpCiAgICBlY2hvICJydW5uaW5nIHdvcmtlciBwb2QiCiAgICBwb2RtYW4gcG9kIHN0YXJ0ICR3b3JrZXJfcG9kX25hbWUKICAgIGlmIFsgJD8gLWVxIDAgXTsgdGhlbgogICAgICAgIGVjaG8gIk9PVCBrZXJuZWwgbW9kdWxlICRrZXJuZWxfbW9kdWxlIGlzIGluc2VydGVkIgogICAgZWxzZQogICAgICAgIGVjaG8gImZhaWxlZCB0byBpbnNlcnQgT09UIGtlcm5lbCBtb2R1bGUgJGtlcm5lbF9tb2R1bGUiCiAgICBmaQogICAgcG9kbWFuIHdhaXQgJHdvcmtlcl9wb2RfaWQKICAgIGVjaG8gInJlbW92aW5nIHZvbHVtZSIKICAgIHBvZG1hbiB2b2x1bWUgcm0gJHdvcmtlcl92b2x1bWVfbmFtZQplbHNlCiAgICBlY2hvICJJbWFnZSAkZnVsbF9rZXJuZWxfbW9kdWxlX2ltYWdlIGlzIG5vdCBwcmVzZW50IGluIGxvY2FsIHJlZ2lzdHJ5LCB3aWxsIHRyeSBhZnRlciByZWJvb3QiCmZpCg==" + source: "data:text/plain;base64,IyEvYmluL2Jhc2gKCmttbV9jb25maWdfZmlsZV9maWxlcGF0aD0iJFdPUktFUl9DT05GSUdfRklMRVBBVEgiCmluX3RyZWVfbW9kdWxlX3RvX3JlbW92ZT0iJElOX1RSRUVfTU9EVUxFX1RPX1JFTU9WRSIKa2VybmVsX21vZHVsZT0iJEtFUk5FTF9NT0RVTEUiCndvcmtlcl9pbWFnZT0iJFdPUktFUl9JTUFHRSIKa2VybmVsX21vZHVsZV9pbWFnZT0iJEtFUk5FTF9NT0RVTEVfSU1BR0UiCmZpcm13YXJlX2ZpbGVzX3BhdGg9IiRGSVJNV0FSRV9GSUxFU19QQVRIIgprZXJuZWxfbW9kdWxlX2ltYWdlX3RhZz0kKHVuYW1lIC1yKQpmdWxsX2tlcm5lbF9tb2R1bGVfaW1hZ2U9IiRrZXJuZWxfbW9kdWxlX2ltYWdlOiRrZXJuZWxfbW9kdWxlX2ltYWdlX3RhZyIKd29ya2VyX3BvZF9uYW1lPWttbS1wb2QKd29ya2VyX3ZvbHVtZV9uYW1lPWttbS12b2x1bWUKCmNyZWF0ZV9rbW1fY29uZmlnKCkgewogICAgIyBXcml0ZSBZQU1MIGNvbnRlbnQgdG8gdGhlIGZpbGUKICAgIGNhdCA8PEVPRiA+ICIka21tX2NvbmZpZ19maWxlX2ZpbGVwYXRoIgpjb250YWluZXJJbWFnZTogJGZ1bGxfa2VybmVsX21vZHVsZV9pbWFnZQppblRyZWVNb2R1bGVUb1JlbW92ZTogJGluX3RyZWVfbW9kdWxlX3RvX3JlbW92ZQptb2Rwcm9iZToKICBkaXJOYW1lOiAvb3B0CiAgbW9kdWxlTmFtZTogJGtlcm5lbF9tb2R1bGUKRU9GCiAgICBlY2hvICJsb2dnaW5nIGNvbnRlbnRzIG9mIHRoZSB3b3JrZXIgY29uZmlnIGZpbGU6IgogICAgY2F0ICIka21tX2NvbmZpZ19maWxlX2ZpbGVwYXRoIgp9CgplY2hvICJiZWZvcmUgY2hlY2tpbmcgaW1hZ2UgcHJlc2VuY2UiCmlmIFsgLW4gIiQocG9kbWFuIGltYWdlcyAtcSAkZnVsbF9rZXJuZWxfbW9kdWxlX2ltYWdlIDI+IC9kZXYvbnVsbCkiIF07IHRoZW4KICAgIGVjaG8gIkltYWdlICRmdWxsX2tlcm5lbF9tb2R1bGVfaW1hZ2UgZm91bmQgb24gdGhlIGxvY2FsIGZpbGUgc3lzdGVtLCBjcmVhdGluZyBrbW0gY29uZmlnIGZpbGUiCiAgICBjcmVhdGVfa21tX2NvbmZpZwogICAgZWNobyAiY3JlYXRpbmcgdm9sdW1lIgogICAgcG9kbWFuIHZvbHVtZSBjcmVhdGUgJHdvcmtlcl92b2x1bWVfbmFtZQogICAgcG9kbWFuIHBvZCBjcmVhdGUgLS1uYW1lICR3b3JrZXJfcG9kX25hbWUKICAgIGVjaG8gImNyZWF0aW5nIGluaXQgY29udGFpbmVyIgogICAgY29weWNtZD0ibWtkaXIgLXAgL3RtcC9vcHQvbGliL21vZHVsZXMgJiYgY3AgLVIgL29wdC9saWIvbW9kdWxlcy8qIC90bXAvb3B0L2xpYi9tb2R1bGVzOyIKICAgIGlmIFtbIC1uICIkRklSTVdBUkVfRklMRVNfUEFUSCIgXV07IHRoZW4KICAgICAgZm9sZGVycz0oInRtcCIgIiRmaXJtd2FyZV9maWxlc19wYXRoIik7CiAgICAgIHBhdGhfdG9fY29weV9maXJtd2FyZT0kKHByaW50ZiAnLyVzJyAiJHtmb2xkZXJzW0BdJS99IikKICAgICAgY29weWNtZCs9IiBta2RpciAtcCAke3BhdGhfdG9fY29weV9maXJtd2FyZX0gJiYgXAogICAgICBjcCAtUiAke2Zpcm13YXJlX2ZpbGVzX3BhdGh9LyogJHtwYXRoX3RvX2NvcHlfZmlybXdhcmV9IgogICAgZmkKICAgIHBvZG1hbiBjcmVhdGUgXAogICAgICAgICAgLS1wb2QgJHdvcmtlcl9wb2RfbmFtZSBcCiAgICAgICAgICAtLWluaXQtY3RyPWFsd2F5cyBcCiAgICAgICAgICAtLXJtIFwKICAgICAgICAgIC12ICR3b3JrZXJfdm9sdW1lX25hbWU6L3RtcCBcCiAgICAgICAgICAkZnVsbF9rZXJuZWxfbW9kdWxlX2ltYWdlIFwKICAgICAgICAgIC9iaW4vc2ggLWMgIiR7Y29weWNtZH0iCiAgICBlY2hvICJjcmVhdGluZyB3b3JrZXIgY29udGFpbmVyIgogICAgd29ya2VyX3BvZF9pZD0kKAogICAgcG9kbWFuIGNyZWF0ZSBcCiAgICAgIC0tcG9kICR3b3JrZXJfcG9kX25hbWVcCiAgICAgIC0tdXNlcj1yb290IFwKICAgICAgLS1wcml2aWxlZ2VkIFwKICAgICAgLS1ybSBcCiAgICAgIC12ICR3b3JrZXJfdm9sdW1lX25hbWU6L3RtcCBcCiAgICAgIC12IC9saWIvbW9kdWxlczovbGliL21vZHVsZXMgXAogICAgICAtdiAka21tX2NvbmZpZ19maWxlX2ZpbGVwYXRoOi9ldGMva21tLXdvcmtlci9jb25maWcueWFtbCBcCiAgICAgICR3b3JrZXJfaW1hZ2UgXAogICAgICBrbW9kIGxvYWQgL2V0Yy9rbW0td29ya2VyL2NvbmZpZy55YW1sKQogICAgZWNobyAicnVubmluZyB3b3JrZXIgcG9kIgogICAgcG9kbWFuIHBvZCBzdGFydCAkd29ya2VyX3BvZF9uYW1lCiAgICBpZiBbICQ/IC1lcSAwIF07IHRoZW4KICAgICAgICBlY2hvICJPT1Qga2VybmVsIG1vZHVsZSAka2VybmVsX21vZHVsZSBpcyBpbnNlcnRlZCIKICAgIGVsc2UKICAgICAgICBlY2hvICJmYWlsZWQgdG8gaW5zZXJ0IE9PVCBrZXJuZWwgbW9kdWxlICRrZXJuZWxfbW9kdWxlIgogICAgZmkKICAgIHBvZG1hbiB3YWl0ICR3b3JrZXJfcG9kX2lkCiAgICBlY2hvICJyZW1vdmluZyBrbW0tcG9kIgogICAgcG9kbWFuIHBvZCBybSAkd29ya2VyX3BvZF9uYW1lCiAgICBlY2hvICJyZW1vdmluZyB2b2x1bWUiCiAgICBwb2RtYW4gdm9sdW1lIHJtICR3b3JrZXJfdm9sdW1lX25hbWUKZWxzZQogICAgZWNobyAiSW1hZ2UgJGZ1bGxfa2VybmVsX21vZHVsZV9pbWFnZSBpcyBub3QgcHJlc2VudCBpbiBsb2NhbCByZWdpc3RyeSwgd2lsbCB0cnkgYWZ0ZXIgcmVib290IgpmaQo=" - path: "/usr/local/bin/pull-kernel-module-image.sh" mode: 493 overwrite: true