From ddfeab2b5d0d7462b0e051396904ac0ce799ba9b Mon Sep 17 00:00:00 2001 From: Bhoopesh Date: Fri, 20 Sep 2024 01:13:52 +0530 Subject: [PATCH 1/2] wip: status command Signed-off-by: Bhoopesh --- sztp-agent/pkg/secureagent/status.go | 253 ++++++++++++++++++++++++++- 1 file changed, 249 insertions(+), 4 deletions(-) diff --git a/sztp-agent/pkg/secureagent/status.go b/sztp-agent/pkg/secureagent/status.go index e5341fdf..099484cd 100644 --- a/sztp-agent/pkg/secureagent/status.go +++ b/sztp-agent/pkg/secureagent/status.go @@ -8,19 +8,264 @@ Copyright (C) 2022 Red Hat. // Package secureagent implements the secure agent package secureagent -import "log" +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "time" +) + +const ( + statusFilePath = "/var/lib/sztp/status.json" + resultFilePath = "/var/lib/sztp/result.json" + symlinkDir = "/run/sztp" +) + +// Status represents the structure of status.json +// Status represents the structure of status.json +type Status struct { + Init StageStatus `json:"init"` + DownloadingFile StageStatus `json:"downloading-file"` + WaitingDHCP string `json:"waiting-dhcp"` + PendingReboot StageStatus `json:"pending-reboot"` + IsCompleted StageStatus `json:"is-completed"` + DataSource string `json:"datasource"` + Stage string `json:"stage"` +} + +// Result represents the structure of result.json +type Result struct { + DataSource string `json:"datasource"` + Errors []string `json:"errors"` +} + +// StageStatus holds the status for each stage of onboarding +type StageStatus struct { + Errors []string `json:"errors"` + Start float64 `json:"start"` + End float64 `json:"end"` +} + +// LoadStatusFile loads the current status.json from the filesystem. +func LoadStatusFile() (*Status, error) { + file, err := os.ReadFile(statusFilePath) + if err != nil { + return nil, err + } + var status Status + err = json.Unmarshal(file, &status) + if err != nil { + return nil, err + } + return &status, nil +} + +// UpdateAndSaveStatus updates the specific part of the status object based on the current stage. +func UpdateAndSaveStatus(stage string, isStart bool, errMsg string) error { + status, err := LoadStatusFile() + if err != nil { + fmt.Println("Creating a new status file.") + status = &Status{ + DataSource: "ds", + Stage: "", + } + } + + now := float64(time.Now().Unix()) + switch stage { + case "init": + if isStart { + status.Init.Start = now + status.Init.End = 0 + } else { + status.Init.End = now + if errMsg != "" { + status.Init.Errors = append(status.Init.Errors, errMsg) + } + } + case "downloading-file": + if isStart { + status.DownloadingFile.Start = now + status.DownloadingFile.End = 0 + } else { + status.DownloadingFile.End = now + if errMsg != "" { + status.DownloadingFile.Errors = append(status.DownloadingFile.Errors, errMsg) + } + } + case "waiting-dhcp": + if isStart { + status.WaitingDHCP = "in-progress" + } else { + status.WaitingDHCP = "completed" + } + case "pending-reboot": + if isStart { + status.PendingReboot.Start = now + status.PendingReboot.End = 0 + } else { + status.PendingReboot.End = now + if errMsg != "" { + status.PendingReboot.Errors = append(status.PendingReboot.Errors, errMsg) + } + } + case "is-completed": + if isStart { + status.IsCompleted.Start = now + status.IsCompleted.End = 0 + } else { + status.IsCompleted.End = now + if errMsg != "" { + status.IsCompleted.Errors = append(status.IsCompleted.Errors, errMsg) + } + } + } + + // Update the current stage + if isStart { + status.Stage = stage + } else { + status.Stage = "" + } + + tempPath := statusFilePath + ".tmp" + file, err := os.Create(tempPath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + if err := encoder.Encode(status); err != nil { + return err + } + + // Atomic move of temp file to replace the original. + return os.Rename(tempPath, statusFilePath) +} + +// SaveResultFile writes the result.json file after provisioning is complete. +func SaveResultFile(result *Result) error { + tempPath := resultFilePath + ".tmp" + file, err := os.Create(tempPath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + if err := encoder.Encode(result); err != nil { + return err + } + + // Atomic move of temp file to replace the original. + return os.Rename(tempPath, resultFilePath) +} + +// EnsureDirExists checks if a directory exists, and creates it if it doesn't. +func EnsureDirExists(dir string) error { + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, 0755) // Create the directory with appropriate permissions + if err != nil { + return fmt.Errorf("failed to create directory %s: %v", dir, err) + } + } + return nil +} + +// EnsureFile ensures that a file exists; creates it if it does not. +func EnsureFileExists(filePath string) error { + // Ensure the directory exists + dir := filepath.Dir(filePath) + if err := EnsureDirExists(dir); err != nil { + return err + } + + // Check if the file already exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + // File does not exist, create it + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file %s: %v", filePath, err) + } + defer file.Close() + fmt.Printf("File %s created successfully.\n", filePath) + } else { + fmt.Printf("File %s already exists.\n", filePath) + } + return nil +} + +// CreateSymlink creates a symlink for a file from target to link location. +func CreateSymlink(targetFile, linkFile string) error { + // Ensure the directory for the symlink exists + linkDir := filepath.Dir(linkFile) + if err := EnsureDirExists(linkDir); err != nil { + return err + } + + // Remove any existing symlink + if _, err := os.Lstat(linkFile); err == nil { + os.Remove(linkFile) + } + + // Create a new symlink + return os.Symlink(targetFile, linkFile) +} // RunCommandStatus runs the command in the background func (a *Agent) RunCommandStatus() error { + if err := a.prepareStatus(); err != nil { + return err + } log.Println("RunCommandStatus") return nil } -/* -func (a *Agent) prepareEnvStatus() error { - log.Println("prepareEnvStatus") +func (a *Agent) prepareStatus() error { + log.Println("prepareStatus") + + // Ensure /run/sztp directory exists + if err := EnsureDirExists(symlinkDir); err != nil { + fmt.Printf("Failed to create directory %s: %v\n", symlinkDir, err) + return err + } + + // Ensure files are created + if err := EnsureFileExists(statusFilePath); err != nil { + return err + } + if err := EnsureFileExists(resultFilePath); err != nil { + return err + } + + // Define symlink paths + statusSymlinkPath := filepath.Join(symlinkDir, "status.json") + resultSymlinkPath := filepath.Join(symlinkDir, "result.json") + + // Create symlinks for status.json and result.json + if err := CreateSymlink(statusFilePath, statusSymlinkPath); err != nil { + fmt.Printf("Failed to create symlink for status.json: %v\n", err) + return err + } + if err := CreateSymlink(resultFilePath, resultSymlinkPath); err != nil { + fmt.Printf("Failed to create symlink for result.json: %v\n", err) + return err + } + + fmt.Println("Symlinks created successfully.") + + // Update the status file + if err := UpdateAndSaveStatus("init", true, ""); err != nil { + return err + } + return nil } + +/* func (a *Agent) configureStatus() error { log.Println("configureStatus") return nil From 951c0a47b0402e3d5ef41349fb5d75c91d5388d9 Mon Sep 17 00:00:00 2001 From: Bhoopesh Date: Mon, 14 Oct 2024 17:10:32 +0530 Subject: [PATCH 2/2] feat: status command Signed-off-by: Bhoopesh --- docker-compose.dpu.yml | 28 ++- scripts/run_agent.sh | 3 + sztp-agent/cmd/daemon.go | 19 +- sztp-agent/cmd/disable.go | 9 +- sztp-agent/cmd/enable.go | 8 +- sztp-agent/cmd/run.go | 19 +- sztp-agent/cmd/status.go | 10 +- sztp-agent/pkg/secureagent/agent.go | 33 ++- sztp-agent/pkg/secureagent/agent_test.go | 11 +- sztp-agent/pkg/secureagent/configuration.go | 4 + sztp-agent/pkg/secureagent/daemon.go | 7 + sztp-agent/pkg/secureagent/image.go | 2 + sztp-agent/pkg/secureagent/progress_test.go | 2 +- sztp-agent/pkg/secureagent/run.go | 4 + sztp-agent/pkg/secureagent/status.go | 222 +++++++++----------- sztp-agent/pkg/secureagent/status_test.go | 10 + sztp-agent/pkg/secureagent/utils.go | 18 ++ 17 files changed, 271 insertions(+), 138 deletions(-) diff --git a/docker-compose.dpu.yml b/docker-compose.dpu.yml index 28c9600f..5e281dda 100644 --- a/docker-compose.dpu.yml +++ b/docker-compose.dpu.yml @@ -43,6 +43,9 @@ services: - dhcp-leases-folder:/var/lib/dhclient/ - /etc/os-release:/etc/os-release - /etc/ssh:/etc/ssh + - /var/lib/sztp:/var/lib/sztp + - /run/sztp:/run/sztp + privileged: true networks: - opi command: ['/opi-sztp-agent', 'daemon', @@ -50,7 +53,10 @@ services: '--bootstrap-trust-anchor-cert', '/certs/opi.pem', '--device-end-entity-cert', '/certs/third_my_cert.pem', '--device-private-key', '/certs/third_private_key.pem', - '--serial-number', 'third-serial-number'] + '--serial-number', 'third-serial-number', + '--status-file-path', '/var/lib/sztp/status.json', + '--result-file-path', '/var/lib/sztp/result.json', + '--sym-link-dir', '/run/sztp'] agent2: <<: *agent @@ -59,7 +65,10 @@ services: '--bootstrap-trust-anchor-cert', '/certs/opi.pem', '--device-end-entity-cert', '/certs/second_my_cert.pem', '--device-private-key', '/certs/second_private_key.pem', - '--serial-number', 'second-serial-number'] + '--serial-number', 'second-serial-number', + '--status-file-path', '/var/lib/sztp/status.json', + '--result-file-path', '/var/lib/sztp/result.json', + '--sym-link-dir', '/run/sztp'] agent1: <<: *agent @@ -68,7 +77,10 @@ services: '--bootstrap-trust-anchor-cert', '/certs/opi.pem', '--device-end-entity-cert', '/certs/first_my_cert.pem', '--device-private-key', '/certs/first_private_key.pem', - '--serial-number', 'first-serial-number'] + '--serial-number', 'first-serial-number', + '--status-file-path', '/var/lib/sztp/status.json', + '--result-file-path', '/var/lib/sztp/result.json', + '--sym-link-dir', '/run/sztp'] agent4: <<: *agent @@ -77,7 +89,10 @@ services: '--bootstrap-trust-anchor-cert', '/certs/opi.pem', '--device-end-entity-cert', '/certs/first_my_cert.pem', '--device-private-key', '/certs/first_private_key.pem', - '--serial-number', 'first-serial-number'] + '--serial-number', 'first-serial-number', + '--status-file-path', '/var/lib/sztp/status.json', + '--result-file-path', '/var/lib/sztp/result.json', + '--sym-link-dir', '/run/sztp'] agent5: <<: *agent @@ -86,7 +101,10 @@ services: '--bootstrap-trust-anchor-cert', '/certs/opi.pem', '--device-end-entity-cert', '/certs/first_my_cert.pem', '--device-private-key', '/certs/first_private_key.pem', - '--serial-number', 'first-serial-number'] + '--serial-number', 'first-serial-number', + '--status-file-path', '/var/lib/sztp/status.json', + '--result-file-path', '/var/lib/sztp/result.json', + '--sym-link-dir', '/run/sztp'] volumes: client-certs: diff --git a/scripts/run_agent.sh b/scripts/run_agent.sh index 7b31a631..2c35965c 100755 --- a/scripts/run_agent.sh +++ b/scripts/run_agent.sh @@ -21,6 +21,9 @@ docker run --rm -it --network=host \ --mount type=bind,source=/etc/ssh,target=/etc/ssh,readonly \ --mount type=bind,source=/etc/os-release,target=/etc/os-release,readonly \ --mount type=bind,source=/var/lib/NetworkManager,target=/var/lib/NetworkManager,readonly \ + --mount type=bind,source=/var/lib/sztp,target=/var/lib/sztp \ + --mount type=bind,source=/run/sztp,target=/run/sztp \ + --privileged \ ${DOCKER_SZTP_IMAGE} \ /opi-sztp-agent daemon \ --dhcp-lease-file /var/lib/NetworkManager/dhclient-eth0.lease \ diff --git a/sztp-agent/cmd/daemon.go b/sztp-agent/cmd/daemon.go index d309d9a6..14521665 100644 --- a/sztp-agent/cmd/daemon.go +++ b/sztp-agent/cmd/daemon.go @@ -32,13 +32,16 @@ func Daemon() *cobra.Command { devicePrivateKey string deviceEndEntityCert string bootstrapTrustAnchorCert string + statusFilePath string + resultFilePath string + symLinkDir string ) cmd := &cobra.Command{ Use: "daemon", Short: "Run the daemon command", RunE: func(_ *cobra.Command, _ []string) error { - arrayChecker := []string{devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert} + arrayChecker := []string{devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert, statusFilePath} if bootstrapURL != "" && dhcpLeaseFile != "" { return fmt.Errorf("'--bootstrap-url' and '--dhcp-lease-file' are mutualy exclusive") } @@ -52,6 +55,15 @@ func Daemon() *cobra.Command { _, err := url.ParseRequestURI(bootstrapURL) cobra.CheckErr(err) } + if statusFilePath == "" { + return fmt.Errorf("'--status-file-path' is required") + } + if resultFilePath == "" { + return fmt.Errorf("'--result-file-path' is required") + } + if symLinkDir == "" { + return fmt.Errorf("'--symlink-dir' is required") + } for _, filePath := range arrayChecker { info, err := os.Stat(filePath) cobra.CheckErr(err) @@ -59,7 +71,7 @@ func Daemon() *cobra.Command { return fmt.Errorf("must not be folder: %q", filePath) } } - a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert) + a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert, statusFilePath, resultFilePath, symLinkDir) return a.RunCommandDaemon() }, } @@ -74,6 +86,9 @@ func Daemon() *cobra.Command { flags.StringVar(&devicePrivateKey, "device-private-key", "/certs/private_key.pem", "Device's private key") flags.StringVar(&deviceEndEntityCert, "device-end-entity-cert", "/certs/my_cert.pem", "Device's End Entity cert") flags.StringVar(&bootstrapTrustAnchorCert, "bootstrap-trust-anchor-cert", "/certs/opi.pem", "Bootstrap server trust anchor Cert") + flags.StringVar(&statusFilePath, "status-file-path", "/var/lib/sztp/status.json", "Path to the status file") + flags.StringVar(&resultFilePath, "result-file-path", "/var/lib/sztp/result.json", "Path to the result file") + flags.StringVar(&symLinkDir, "sym-link-dir", "/run/sztp", "Path to the symlink directory") return cmd } diff --git a/sztp-agent/cmd/disable.go b/sztp-agent/cmd/disable.go index 3ce70a4d..325b2029 100644 --- a/sztp-agent/cmd/disable.go +++ b/sztp-agent/cmd/disable.go @@ -28,13 +28,16 @@ func Disable() *cobra.Command { devicePrivateKey string deviceEndEntityCert string bootstrapTrustAnchorCert string + statusFilePath string + resultFilePath string + symLinkDir string ) cmd := &cobra.Command{ Use: "disable", Short: "Run the disable command", RunE: func(_ *cobra.Command, _ []string) error { - a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert) + a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert, statusFilePath, resultFilePath, symLinkDir) return a.RunCommandDisable() }, } @@ -49,5 +52,9 @@ func Disable() *cobra.Command { flags.StringVar(&devicePrivateKey, "device-private-key", "", "Device's private key") flags.StringVar(&deviceEndEntityCert, "device-end-entity-cert", "", "Device's End Entity cert") flags.StringVar(&bootstrapTrustAnchorCert, "bootstrap-trust-anchor-cert", "", "Bootstrap server trust anchor Cert") + flags.StringVar(&statusFilePath, "status-file-path", "", "Status file path") + flags.StringVar(&resultFilePath, "result-file-path", "", "Result file path") + flags.StringVar(&symLinkDir, "sym-link-dir", "", "Sym Link Directory") + return cmd } diff --git a/sztp-agent/cmd/enable.go b/sztp-agent/cmd/enable.go index 745bd795..35defbed 100644 --- a/sztp-agent/cmd/enable.go +++ b/sztp-agent/cmd/enable.go @@ -28,13 +28,16 @@ func Enable() *cobra.Command { devicePrivateKey string deviceEndEntityCert string bootstrapTrustAnchorCert string + statusFilePath string + resultFilePath string + symLinkDir string ) cmd := &cobra.Command{ Use: "enable", Short: "Run the enable command", RunE: func(_ *cobra.Command, _ []string) error { - a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert) + a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert, statusFilePath, resultFilePath, symLinkDir) return a.RunCommandEnable() }, } @@ -49,6 +52,9 @@ func Enable() *cobra.Command { flags.StringVar(&devicePrivateKey, "device-private-key", "", "Device's private key") flags.StringVar(&deviceEndEntityCert, "device-end-entity-cert", "", "Device's End Entity cert") flags.StringVar(&bootstrapTrustAnchorCert, "bootstrap-trust-anchor-cert", "", "Bootstrap server trust anchor Cert") + flags.StringVar(&statusFilePath, "status-file-path", "", "Status file path") + flags.StringVar(&resultFilePath, "result-file-path", "", "Result file path") + flags.StringVar(&symLinkDir, "sym-link-dir", "", "Sym Link Directory") return cmd } diff --git a/sztp-agent/cmd/run.go b/sztp-agent/cmd/run.go index f3b02c1f..730fc7f6 100644 --- a/sztp-agent/cmd/run.go +++ b/sztp-agent/cmd/run.go @@ -32,13 +32,16 @@ func Run() *cobra.Command { devicePrivateKey string deviceEndEntityCert string bootstrapTrustAnchorCert string + statusFilePath string + resultFilePath string + symLinkDir string ) cmd := &cobra.Command{ Use: "run", Short: "Exec the run command", RunE: func(_ *cobra.Command, _ []string) error { - arrayChecker := []string{devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert} + arrayChecker := []string{devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert, statusFilePath, resultFilePath} if bootstrapURL != "" && dhcpLeaseFile != "" { return fmt.Errorf("'--bootstrap-url' and '--dhcp-lease-file' are mutualy exclusive") } @@ -52,6 +55,15 @@ func Run() *cobra.Command { _, err := url.ParseRequestURI(bootstrapURL) cobra.CheckErr(err) } + if statusFilePath == "" { + return fmt.Errorf("'--status-file-path' is required") + } + if resultFilePath == "" { + return fmt.Errorf("'--result-file-path' is required") + } + if symLinkDir == "" { + return fmt.Errorf("'--symlink-dir' is required") + } for _, filePath := range arrayChecker { info, err := os.Stat(filePath) cobra.CheckErr(err) @@ -59,7 +71,7 @@ func Run() *cobra.Command { return fmt.Errorf("must not be folder: %q", filePath) } } - a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert) + a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert, statusFilePath, resultFilePath, symLinkDir) return a.RunCommand() }, } @@ -74,6 +86,9 @@ func Run() *cobra.Command { flags.StringVar(&devicePrivateKey, "device-private-key", "/certs/private_key.pem", "Device's private key") flags.StringVar(&deviceEndEntityCert, "device-end-entity-cert", "/certs/my_cert.pem", "Device's End Entity cert") flags.StringVar(&bootstrapTrustAnchorCert, "bootstrap-trust-anchor-cert", "/certs/opi.pem", "Bootstrap server trust anchor Cert") + flags.StringVar(&statusFilePath, "status-file-path", "/var/lib/sztp/status.json", "Status file path") + flags.StringVar(&resultFilePath, "result-file-path", "/var/lib/sztp/result.json", "Result file path") + flags.StringVar(&symLinkDir, "sym-link-dir", "", "Sym Link Directory") return cmd } diff --git a/sztp-agent/cmd/status.go b/sztp-agent/cmd/status.go index cf5043a7..2da7c870 100644 --- a/sztp-agent/cmd/status.go +++ b/sztp-agent/cmd/status.go @@ -28,13 +28,16 @@ func Status() *cobra.Command { devicePrivateKey string deviceEndEntityCert string bootstrapTrustAnchorCert string + statusFilePath string + resultFilePath string + symLinkDir string ) cmd := &cobra.Command{ Use: "status", Short: "Run the status command", RunE: func(_ *cobra.Command, _ []string) error { - a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert) + a := secureagent.NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert, statusFilePath, resultFilePath, symLinkDir) return a.RunCommandStatus() }, } @@ -45,10 +48,13 @@ func Status() *cobra.Command { flags.StringVar(&bootstrapURL, "bootstrap-url", "", "Bootstrap server URL") flags.StringVar(&serialNumber, "serial-number", "", "Device's serial number") flags.StringVar(&dhcpLeaseFile, "dhcp-lease-file", "/var/lib/dhclient/dhclient.leases", "Device's dhclient leases file") - flags.StringVar(&devicePassword, "device-password", "", "Device's password") + flags.StringVar(&devicePassword, "device-password", "", "Dehomevice's password") flags.StringVar(&devicePrivateKey, "device-private-key", "", "Device's private key") flags.StringVar(&deviceEndEntityCert, "device-end-entity-cert", "", "Device's End Entity cert") flags.StringVar(&bootstrapTrustAnchorCert, "bootstrap-trust-anchor-cert", "", "Bootstrap server trust anchor Cert") + flags.StringVar(&statusFilePath, "status-file-path", "", "Status file path") + flags.StringVar(&resultFilePath, "result-file-path", "", "Result file path") + flags.StringVar(&symLinkDir, "sym-link-dir", "", "Sym Link Directory") return cmd } diff --git a/sztp-agent/pkg/secureagent/agent.go b/sztp-agent/pkg/secureagent/agent.go index e248dc42..f1f26178 100644 --- a/sztp-agent/pkg/secureagent/agent.go +++ b/sztp-agent/pkg/secureagent/agent.go @@ -83,10 +83,12 @@ type Agent struct { ProgressJSON ProgressJSON // ProgressJson structure BootstrapServerOnboardingInfo BootstrapServerOnboardingInfo // BootstrapServerOnboardingInfo structure BootstrapServerRedirectInfo BootstrapServerRedirectInfo // BootstrapServerRedirectInfo structure - + StatusFilePath string // Path to the status file + ResultFilePath string // Path to the result file + SymLinkDir string // Path to the symlink directory for the status file } -func NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert string) *Agent { +func NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, devicePrivateKey, deviceEndEntityCert, bootstrapTrustAnchorCert, statusFilePath, resultFilePath, symLinkDir string) *Agent { return &Agent{ InputBootstrapURL: bootstrapURL, BootstrapURL: "", @@ -101,6 +103,9 @@ func NewAgent(bootstrapURL, serialNumber, dhcpLeaseFile, devicePassword, deviceP ProgressJSON: ProgressJSON{}, BootstrapServerRedirectInfo: BootstrapServerRedirectInfo{}, BootstrapServerOnboardingInfo: BootstrapServerOnboardingInfo{}, + StatusFilePath: statusFilePath, + ResultFilePath: resultFilePath, + SymLinkDir: symLinkDir, } } @@ -140,6 +145,18 @@ func (a *Agent) GetProgressJSON() ProgressJSON { return a.ProgressJSON } +func (a *Agent) GetStatusFilePath() string { + return a.StatusFilePath +} + +func (a *Agent) GetResultFilePath() string { + return a.ResultFilePath +} + +func (a *Agent) GetSymLinkDir() string { + return a.SymLinkDir +} + func (a *Agent) SetBootstrapURL(url string) { a.BootstrapURL = url } @@ -171,3 +188,15 @@ func (a *Agent) SetContentTypeReq(ct string) { func (a *Agent) SetProgressJSON(p ProgressJSON) { a.ProgressJSON = p } + +func (a *Agent) SetStatusFilePath(path string) { + a.StatusFilePath = path +} + +func (a *Agent) SetResultFilePath(path string) { + a.ResultFilePath = path +} + +func (a *Agent) SetSymLinkDir(path string) { + a.SymLinkDir = path +} diff --git a/sztp-agent/pkg/secureagent/agent_test.go b/sztp-agent/pkg/secureagent/agent_test.go index ad2ebb09..0ae3979f 100644 --- a/sztp-agent/pkg/secureagent/agent_test.go +++ b/sztp-agent/pkg/secureagent/agent_test.go @@ -828,6 +828,9 @@ func TestNewAgent(t *testing.T) { devicePrivateKey string deviceEndEntityCert string bootstrapTrustAnchorCert string + statusFilePath string + resultFilePath string + symLinkDir string } tests := []struct { name string @@ -844,6 +847,9 @@ func TestNewAgent(t *testing.T) { devicePrivateKey: "TestDevicePrivateKey", deviceEndEntityCert: "TestDeviceEndEntityCert", bootstrapTrustAnchorCert: "TestBootstrapTrustCert", + statusFilePath: "TestStatusFilePath", + resultFilePath: "TestResultFilePath", + symLinkDir: "TestSymLinkDir", }, want: &Agent{ InputBootstrapURL: "TestBootstrap", @@ -856,12 +862,15 @@ func TestNewAgent(t *testing.T) { ContentTypeReq: "application/yang-data+json", InputJSONContent: generateInputJSONContent(), DhcpLeaseFile: "TestDhcpLeaseFile", + StatusFilePath: "TestStatusFilePath", + ResultFilePath: "TestResultFilePath", + SymLinkDir: "TestSymLinkDir", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := NewAgent(tt.args.bootstrapURL, tt.args.serialNumber, tt.args.dhcpLeaseFile, tt.args.devicePassword, tt.args.devicePrivateKey, tt.args.deviceEndEntityCert, tt.args.bootstrapTrustAnchorCert); !reflect.DeepEqual(got, tt.want) { + if got := NewAgent(tt.args.bootstrapURL, tt.args.serialNumber, tt.args.dhcpLeaseFile, tt.args.devicePassword, tt.args.devicePrivateKey, tt.args.deviceEndEntityCert, tt.args.bootstrapTrustAnchorCert, tt.args.statusFilePath, tt.args.resultFilePath, tt.args.symLinkDir); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewAgent() = %v, want %v", got, tt.want) } }) diff --git a/sztp-agent/pkg/secureagent/configuration.go b/sztp-agent/pkg/secureagent/configuration.go index 1a6f0133..00a569a6 100644 --- a/sztp-agent/pkg/secureagent/configuration.go +++ b/sztp-agent/pkg/secureagent/configuration.go @@ -10,6 +10,7 @@ import ( func (a *Agent) copyConfigurationFile() error { log.Println("[INFO] Starting the Copy Configuration.") _ = a.doReportProgress(ProgressTypeConfigInitiated, "Configuration Initiated") + _ = a.UpdateAndSaveStatus("config", true, "") // Copy the configuration file to the device file, err := os.Create(ARTIFACTS_PATH + a.BootstrapServerOnboardingInfo.IetfSztpConveyedInfoOnboardingInformation.InfoTimestampReference + "-config") if err != nil { @@ -36,6 +37,7 @@ func (a *Agent) copyConfigurationFile() error { } log.Println("[INFO] Configuration file copied successfully") _ = a.doReportProgress(ProgressTypeConfigComplete, "Configuration Complete") + _ = a.UpdateAndSaveStatus("config", false, "") return nil } @@ -56,6 +58,7 @@ func (a *Agent) launchScriptsConfiguration(typeOf string) error { } log.Println("[INFO] Starting the " + scriptName + "-configuration.") _ = a.doReportProgress(reportStart, "Report starting") + _ = a.UpdateAndSaveStatus(scriptName+"-script", true, "") // nolint:gosec file, err := os.Create(ARTIFACTS_PATH + a.BootstrapServerOnboardingInfo.IetfSztpConveyedInfoOnboardingInformation.InfoTimestampReference + scriptName + "configuration.sh") if err != nil { @@ -89,6 +92,7 @@ func (a *Agent) launchScriptsConfiguration(typeOf string) error { } log.Println(string(out)) // remove it _ = a.doReportProgress(reportEnd, "Report end") + _ = a.UpdateAndSaveStatus(scriptName+"-script", false, "") log.Println("[INFO] " + scriptName + "-Configuration script executed successfully") return nil } diff --git a/sztp-agent/pkg/secureagent/daemon.go b/sztp-agent/pkg/secureagent/daemon.go index 2064d90e..a829a324 100644 --- a/sztp-agent/pkg/secureagent/daemon.go +++ b/sztp-agent/pkg/secureagent/daemon.go @@ -33,6 +33,10 @@ const ( // RunCommandDaemon runs the command in the background func (a *Agent) RunCommandDaemon() error { + if err := a.PrepareStatus(); err != nil { + log.Println("failed to prepare status: ", err) + return err + } for { err := a.performBootstrapSequence() if err != nil { @@ -46,6 +50,7 @@ func (a *Agent) RunCommandDaemon() error { } func (a *Agent) performBootstrapSequence() error { + _ = a.UpdateAndSaveStatus("bootstrap", true, "") var err error err = a.discoverBootstrapURLs() if err != nil { @@ -76,6 +81,7 @@ func (a *Agent) performBootstrapSequence() error { return err } _ = a.doReportProgress(ProgressTypeBootstrapComplete, "Bootstrap Complete") + _ = a.UpdateAndSaveStatus("bootstrap", false, "") return nil } @@ -142,6 +148,7 @@ func (a *Agent) doRequestBootstrapServerOnboardingInfo() error { } log.Println("[INFO] Response retrieved successfully") _ = a.doReportProgress(ProgressTypeBootstrapInitiated, "Bootstrap Initiated") + _ = a.UpdateAndSaveStatus("bootstrap", true, "") crypto := res.IetfSztpBootstrapServerOutput.ConveyedInformation newVal, err := base64.StdEncoding.DecodeString(crypto) if err != nil { diff --git a/sztp-agent/pkg/secureagent/image.go b/sztp-agent/pkg/secureagent/image.go index daff80d3..5b8e6fcf 100644 --- a/sztp-agent/pkg/secureagent/image.go +++ b/sztp-agent/pkg/secureagent/image.go @@ -26,6 +26,7 @@ import ( func (a *Agent) downloadAndValidateImage() error { log.Printf("[INFO] Starting the Download Image: %v", a.BootstrapServerOnboardingInfo.IetfSztpConveyedInfoOnboardingInformation.BootImage.DownloadURI) _ = a.doReportProgress(ProgressTypeBootImageInitiated, "BootImage Initiated") + _ = a.UpdateAndSaveStatus("boot-image", true, "") // Download the image from DownloadURI and save it to a file a.BootstrapServerOnboardingInfo.IetfSztpConveyedInfoOnboardingInformation.InfoTimestampReference = fmt.Sprintf("%8d", time.Now().Unix()) for i, item := range a.BootstrapServerOnboardingInfo.IetfSztpConveyedInfoOnboardingInformation.BootImage.DownloadURI { @@ -101,6 +102,7 @@ func (a *Agent) downloadAndValidateImage() error { } log.Println("[INFO] Checksum verified successfully") _ = a.doReportProgress(ProgressTypeBootImageComplete, "BootImage Complete") + _ = a.UpdateAndSaveStatus("boot-image", false, "") return nil default: return errors.New("unsupported hash algorithm") diff --git a/sztp-agent/pkg/secureagent/progress_test.go b/sztp-agent/pkg/secureagent/progress_test.go index 7a55c0cb..af4cd044 100644 --- a/sztp-agent/pkg/secureagent/progress_test.go +++ b/sztp-agent/pkg/secureagent/progress_test.go @@ -142,7 +142,7 @@ func TestAgent_doReportProgress(t *testing.T) { DhcpLeaseFile: "DHCPLEASEFILE", ProgressJSON: ProgressJSON{}, }, - wantErr: true, + wantErr: true, }, } for _, tt := range tests { diff --git a/sztp-agent/pkg/secureagent/run.go b/sztp-agent/pkg/secureagent/run.go index b3b2e4c9..978fc036 100644 --- a/sztp-agent/pkg/secureagent/run.go +++ b/sztp-agent/pkg/secureagent/run.go @@ -13,6 +13,10 @@ import "log" // RunCommand runs the command in the background func (a *Agent) RunCommand() error { log.Println("runCommand started") + if err := a.PrepareStatus(); err != nil { + log.Println("failed to prepare status: ", err) + return err + } err := a.performBootstrapSequence() if err != nil { log.Println("Error in performBootstrapSequence inside runCommand: ", err) diff --git a/sztp-agent/pkg/secureagent/status.go b/sztp-agent/pkg/secureagent/status.go index 099484cd..9dcc15d5 100644 --- a/sztp-agent/pkg/secureagent/status.go +++ b/sztp-agent/pkg/secureagent/status.go @@ -17,31 +17,28 @@ import ( "time" ) -const ( - statusFilePath = "/var/lib/sztp/status.json" - resultFilePath = "/var/lib/sztp/result.json" - symlinkDir = "/run/sztp" -) - -// Status represents the structure of status.json -// Status represents the structure of status.json +// Status represents the status of the provisioning process. type Status struct { Init StageStatus `json:"init"` - DownloadingFile StageStatus `json:"downloading-file"` - WaitingDHCP string `json:"waiting-dhcp"` + DownloadingFile StageStatus `json:"downloading-file"` // not sure if this is needed PendingReboot StageStatus `json:"pending-reboot"` + Parsing StageStatus `json:"parsing"` + BootImage StageStatus `json:"boot-image"` + PreScript StageStatus `json:"pre-script"` + Config StageStatus `json:"config"` + PostScript StageStatus `json:"post-script"` + Bootstrap StageStatus `json:"bootstrap"` IsCompleted StageStatus `json:"is-completed"` + Informational string `json:"informational"` DataSource string `json:"datasource"` Stage string `json:"stage"` } -// Result represents the structure of result.json type Result struct { - DataSource string `json:"datasource"` + DataSource string `json:"dat asource"` Errors []string `json:"errors"` } -// StageStatus holds the status for each stage of onboarding type StageStatus struct { Errors []string `json:"errors"` Start float64 `json:"start"` @@ -49,8 +46,8 @@ type StageStatus struct { } // LoadStatusFile loads the current status.json from the filesystem. -func LoadStatusFile() (*Status, error) { - file, err := os.ReadFile(statusFilePath) +func (a *Agent) loadStatusFile() (*Status, error) { + file, err := os.ReadFile(a.GetStatusFilePath()) if err != nil { return nil, err } @@ -62,106 +59,88 @@ func LoadStatusFile() (*Status, error) { return &status, nil } -// UpdateAndSaveStatus updates the specific part of the status object based on the current stage. -func UpdateAndSaveStatus(stage string, isStart bool, errMsg string) error { - status, err := LoadStatusFile() - if err != nil { - fmt.Println("Creating a new status file.") - status = &Status{ - DataSource: "ds", - Stage: "", - } - } +func (a *Agent) UpdateAndSaveStatus(stage string, isStart bool, errMsg string) error { + status, err := a.loadStatusFile() + if err != nil { + fmt.Println("Creating a new status file.") + status = a.createNewStatus() + } - now := float64(time.Now().Unix()) - switch stage { - case "init": - if isStart { - status.Init.Start = now - status.Init.End = 0 - } else { - status.Init.End = now - if errMsg != "" { - status.Init.Errors = append(status.Init.Errors, errMsg) - } - } - case "downloading-file": - if isStart { - status.DownloadingFile.Start = now - status.DownloadingFile.End = 0 - } else { - status.DownloadingFile.End = now - if errMsg != "" { - status.DownloadingFile.Errors = append(status.DownloadingFile.Errors, errMsg) - } - } - case "waiting-dhcp": - if isStart { - status.WaitingDHCP = "in-progress" - } else { - status.WaitingDHCP = "completed" - } - case "pending-reboot": - if isStart { - status.PendingReboot.Start = now - status.PendingReboot.End = 0 - } else { - status.PendingReboot.End = now - if errMsg != "" { - status.PendingReboot.Errors = append(status.PendingReboot.Errors, errMsg) - } - } - case "is-completed": - if isStart { - status.IsCompleted.Start = now - status.IsCompleted.End = 0 - } else { - status.IsCompleted.End = now - if errMsg != "" { - status.IsCompleted.Errors = append(status.IsCompleted.Errors, errMsg) - } - } - } + if err := a.updateStageStatus(status, stage, isStart, errMsg); err != nil { + return err + } - // Update the current stage - if isStart { - status.Stage = stage - } else { - status.Stage = "" - } + return a.saveStatus(status) +} - tempPath := statusFilePath + ".tmp" - file, err := os.Create(tempPath) - if err != nil { - return err - } - defer file.Close() +// createNewStatus initializes a new Status object when status.json doesn't exist. +func (a *Agent) createNewStatus() *Status { + return &Status{ + DataSource: "ds", + Stage: "", + } +} - encoder := json.NewEncoder(file) - if err := encoder.Encode(status); err != nil { - return err - } +// updateStageStatus updates the status object for a specific stage. +func (a *Agent) updateStageStatus(status *Status, stage string, isStart bool, errMsg string) error { + now := float64(time.Now().Unix()) + + switch stage { + case "init": + updateStage(&status.Init, isStart, now, errMsg) + case "downloading-file": + updateStage(&status.DownloadingFile, isStart, now, errMsg) + case "pending-reboot": + updateStage(&status.PendingReboot, isStart, now, errMsg) + case "is-completed": + updateStage(&status.IsCompleted, isStart, now, errMsg) + case "parsing": + updateStage(&status.Parsing, isStart, now, errMsg) + case "boot-image": + updateStage(&status.BootImage, isStart, now, errMsg) + case "pre-script": + updateStage(&status.PreScript, isStart, now, errMsg) + case "config": + updateStage(&status.Config, isStart, now, errMsg) + case "post-script": + updateStage(&status.PostScript, isStart, now, errMsg) + case "bootstrap": + updateStage(&status.Bootstrap, isStart, now, errMsg) + + default: + return fmt.Errorf("unknown stage: %s", stage) + } + + // Update the current stage + if isStart { + status.Stage = stage + } else { + status.Stage = "" + } - // Atomic move of temp file to replace the original. - return os.Rename(tempPath, statusFilePath) + return nil } -// SaveResultFile writes the result.json file after provisioning is complete. -func SaveResultFile(result *Result) error { - tempPath := resultFilePath + ".tmp" - file, err := os.Create(tempPath) - if err != nil { - return err +func updateStage(stageStatus *StageStatus, isStart bool, now float64, errMsg string) { + if isStart { + stageStatus.Start = now + stageStatus.End = 0 + } else { + stageStatus.End = now + if errMsg != "" { + stageStatus.Errors = append(stageStatus.Errors, errMsg) + } } - defer file.Close() +} - encoder := json.NewEncoder(file) - if err := encoder.Encode(result); err != nil { - return err - } +// SaveStatusToFile saves the Status object to the status.json file. +func (a *Agent) saveStatus(status *Status) error { + return saveToFile(status, a.GetStatusFilePath()) +} - // Atomic move of temp file to replace the original. - return os.Rename(tempPath, resultFilePath) +// SaveResultFile saves the Result object to the result.json file. +func (a *Agent) saveResult(result *Result) error { + return saveToFile(result, a.GetResultFilePath()) } // EnsureDirExists checks if a directory exists, and creates it if it doesn't. @@ -217,48 +196,49 @@ func CreateSymlink(targetFile, linkFile string) error { // RunCommandStatus runs the command in the background func (a *Agent) RunCommandStatus() error { - if err := a.prepareStatus(); err != nil { - return err - } log.Println("RunCommandStatus") + // read the status file and print the status in command line + status, err := a.loadStatusFile() + if err != nil { + log.Println("failed to load status file: ", err) + return err + } + fmt.Printf("Current status: %+v\n", status) return nil } -func (a *Agent) prepareStatus() error { +func (a *Agent) PrepareStatus() error { log.Println("prepareStatus") // Ensure /run/sztp directory exists - if err := EnsureDirExists(symlinkDir); err != nil { - fmt.Printf("Failed to create directory %s: %v\n", symlinkDir, err) + if err := EnsureDirExists(a.GetSymLinkDir()); err != nil { + fmt.Printf("Failed to create directory %s: %v\n", a.GetSymLinkDir(), err) return err } - // Ensure files are created - if err := EnsureFileExists(statusFilePath); err != nil { + if err := EnsureFileExists(a.GetStatusFilePath()); err != nil { return err } - if err := EnsureFileExists(resultFilePath); err != nil { + if err := EnsureFileExists(a.GetResultFilePath()); err != nil { return err } - // Define symlink paths - statusSymlinkPath := filepath.Join(symlinkDir, "status.json") - resultSymlinkPath := filepath.Join(symlinkDir, "result.json") + statusSymlinkPath := filepath.Join(a.GetSymLinkDir(), "status.json") + resultSymlinkPath := filepath.Join(a.GetSymLinkDir(), "result.json") // Create symlinks for status.json and result.json - if err := CreateSymlink(statusFilePath, statusSymlinkPath); err != nil { + if err := CreateSymlink(a.GetStatusFilePath(), statusSymlinkPath); err != nil { fmt.Printf("Failed to create symlink for status.json: %v\n", err) return err } - if err := CreateSymlink(resultFilePath, resultSymlinkPath); err != nil { + if err := CreateSymlink(a.GetResultFilePath(), resultSymlinkPath); err != nil { fmt.Printf("Failed to create symlink for result.json: %v\n", err) return err } fmt.Println("Symlinks created successfully.") - // Update the status file - if err := UpdateAndSaveStatus("init", true, ""); err != nil { + if err := a.UpdateAndSaveStatus("init", true, ""); err != nil { return err } diff --git a/sztp-agent/pkg/secureagent/status_test.go b/sztp-agent/pkg/secureagent/status_test.go index 2aa11331..ece4925e 100644 --- a/sztp-agent/pkg/secureagent/status_test.go +++ b/sztp-agent/pkg/secureagent/status_test.go @@ -20,6 +20,9 @@ func TestAgent_RunCommandStatus(t *testing.T) { ProgressJSON ProgressJSON BootstrapServerOnboardingInfo BootstrapServerOnboardingInfo BootstrapServerRedirectInfo BootstrapServerRedirectInfo + StatusFilePath string + ResultFilePath string + SymLinkDir string } tests := []struct { name string @@ -41,6 +44,9 @@ func TestAgent_RunCommandStatus(t *testing.T) { ProgressJSON: ProgressJSON{}, BootstrapServerRedirectInfo: BootstrapServerRedirectInfo{}, BootstrapServerOnboardingInfo: BootstrapServerOnboardingInfo{}, + StatusFilePath: "/var/lib/sztp/status.json", + ResultFilePath: "/var/lib/sztp/result.json", + SymLinkDir: "/run/sztp", }, }, } @@ -59,7 +65,11 @@ func TestAgent_RunCommandStatus(t *testing.T) { ProgressJSON: tt.fields.ProgressJSON, BootstrapServerOnboardingInfo: tt.fields.BootstrapServerOnboardingInfo, BootstrapServerRedirectInfo: tt.fields.BootstrapServerRedirectInfo, + StatusFilePath: tt.fields.StatusFilePath, + ResultFilePath: tt.fields.ResultFilePath, + SymLinkDir: tt.fields.SymLinkDir, } + a.PrepareStatus() if err := a.RunCommandStatus(); (err != nil) != tt.wantErr { t.Errorf("RunCommandStatus() error = %v, wantErr %v", err, tt.wantErr) } diff --git a/sztp-agent/pkg/secureagent/utils.go b/sztp-agent/pkg/secureagent/utils.go index e9cc7a6b..8ed65069 100644 --- a/sztp-agent/pkg/secureagent/utils.go +++ b/sztp-agent/pkg/secureagent/utils.go @@ -88,3 +88,21 @@ func calculateSHA256File(filePath string) (string, error) { checkSum := fmt.Sprintf("%x", h.Sum(nil)) return checkSum, nil } + +// saveToFile writes the given data to a specified file path. +func saveToFile(data interface{}, filePath string) error { + tempPath := filePath + ".tmp" + file, err := os.Create(tempPath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + if err := encoder.Encode(data); err != nil { + return err + } + + // Atomic move of temp file to replace the original. + return os.Rename(tempPath, filePath) +}