diff --git a/Makefile b/Makefile index 43473830..bf01bf80 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,7 @@ dist: clean internal/connect/version.txt vendor @cp LICENSE LICENSE.LGPL README.md $(DIST) @cp SUSEConnect.example $(DIST) @cp build/packaging/suseconnect-keepalive* $(DIST)/build/packaging + @cp build/packaging/suse-uptime-tracker* $(DIST)/build/packaging @cp -r build/packaging/suseconnect-ng* $(DIST) @tar cfvj vendor.tar.xz vendor @@ -52,6 +53,7 @@ build: clean out internal/connect/version.txt $(GO) build $(GOFLAGS) $(BINFLAGS) $(OUT) github.com/SUSE/connect-ng/cmd/suseconnect $(GO) build $(GOFLAGS) $(BINFLAGS) $(OUT) github.com/SUSE/connect-ng/cmd/zypper-migration $(GO) build $(GOFLAGS) $(BINFLAGS) $(OUT) github.com/SUSE/connect-ng/cmd/zypper-search-packages + $(GO) build $(GOFLAGS) $(BINFLAGS) $(OUT) github.com/SUSE/connect-ng/cmd/suse-uptime-tracker $(GO) build $(GOFLAGS) $(SOFLAGS) $(OUT) github.com/SUSE/connect-ng/third_party/libsuseconnect # This "arm" means ARM64v8 little endian, the one being delivered currently on diff --git a/build/packaging/suse-uptime-tracker.service b/build/packaging/suse-uptime-tracker.service new file mode 100644 index 00000000..2dde2871 --- /dev/null +++ b/build/packaging/suse-uptime-tracker.service @@ -0,0 +1,7 @@ +[Unit] +Description=Run SUSE uptime tracker +Wants=suse-uptime-tracker.timer + +[Service] +Type=oneshot +ExecStart=/usr/bin/suse-uptime-tracker diff --git a/build/packaging/suse-uptime-tracker.timer b/build/packaging/suse-uptime-tracker.timer new file mode 100644 index 00000000..dbea16da --- /dev/null +++ b/build/packaging/suse-uptime-tracker.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Schedule uptime tracking every 15 minutes + +[Timer] +# Run this timer every 15 minutes +OnCalendar=*:0/15 +RandomizedDelaySec=5m + +[Install] +WantedBy=timers.target diff --git a/build/packaging/suseconnect-ng.changes b/build/packaging/suseconnect-ng.changes index e78d98bd..be36868c 100644 --- a/build/packaging/suseconnect-ng.changes +++ b/build/packaging/suseconnect-ng.changes @@ -2,6 +2,7 @@ Fri Sep 13 15:56:05 UTC 2024 - Miquel Sabate Sola - IN PROGRESS: 1.13 + - Integrating uptime-tracker ------------------------------------------------------------------- Fri Sep 13 14:11:22 UTC 2024 - Miquel Sabate Sola diff --git a/build/packaging/suseconnect-ng.spec b/build/packaging/suseconnect-ng.spec index 42d0cd29..88dd9ed4 100644 --- a/build/packaging/suseconnect-ng.spec +++ b/build/packaging/suseconnect-ng.spec @@ -107,6 +107,7 @@ echo %{version} > internal/connect/version.txt go build -v -ldflags "-s -w" -mod=vendor -buildmode=pie -o bin/suseconnect %{project}/cmd/suseconnect go build -v -ldflags "-s -w" -mod=vendor -buildmode=pie -o bin/zypper-migration %{project}/cmd/zypper-migration go build -v -ldflags "-s -w" -mod=vendor -buildmode=pie -o bin/zypper-search-packages %{project}/cmd/zypper-search-packages +go build -v -ldflags "-s -w" -mod=vendor -buildmode=pie -o bin/suse-uptime-tracker %{project}/cmd/suse-uptime-tracker # the library mkdir -p %_builddir/go/lib @@ -115,6 +116,7 @@ go build -v -ldflags "-s -w" -mod=vendor -buildmode=c-shared -o lib/libsuseconne %install # Install binary + symlinks install -D -m 0755 bin/suseconnect %{buildroot}/%{_bindir}/suseconnect +install -D -m 0755 bin/suse-uptime-tracker %{buildroot}/%{_bindir}/suse-uptime-tracker ln -s %{_bindir}/suseconnect %{buildroot}/%{_bindir}/SUSEConnect install -d -m 0755 %{buildroot}/%{_sbindir} @@ -139,13 +141,16 @@ install -D -m 644 SUSEConnect.example %{buildroot}%{_sysconfdir}/SUSEConnect.exa # Install the SUSEConnect --keepalive timer and service. install -D -m 644 build/packaging/suseconnect-keepalive.timer %{buildroot}/%{_unitdir}/suseconnect-keepalive.timer install -D -m 644 build/packaging/suseconnect-keepalive.service %{buildroot}/%{_unitdir}/suseconnect-keepalive.service +install -D -m 644 build/packaging/suse-uptime-tracker.timer %{buildroot}/%{_unitdir}/suse-uptime-tracker.timer +install -D -m 644 build/packaging/suse-uptime-tracker.service %{buildroot}/%{_unitdir}/suse-uptime-tracker.service ln -sf service %{buildroot}/%{_sbindir}/rcsuseconnect-keepalive +ln -sf service %{buildroot}/%{_sbindir}/rcsuse-uptime-tracker # we currently do not ship the source for any go module rm -rf %{buildroot}/usr/share/go %pre -%service_add_pre suseconnect-keepalive.service suseconnect-keepalive.timer +%service_add_pre suseconnect-keepalive.service suseconnect-keepalive.timer suse-uptime-tracker.service suse-uptime-tracker.timer # in pre blocks the old version is still installed. This way we can detect # if --keepalive was already present before @@ -197,13 +202,13 @@ fi sed -i '/RandomizedDelaySec*/d' %{_unitdir}/suseconnect-keepalive.timer sed -i "s/OnCalendar=daily/OnCalendar=*-*-* $TIMER_HOUR:$TIMER_MINUTE:00/" %{_unitdir}/suseconnect-keepalive.timer %endif -%service_add_post suseconnect-keepalive.service suseconnect-keepalive.timer +%service_add_post suseconnect-keepalive.service suseconnect-keepalive.timer suse-uptime-tracker.service suse-uptime-tracker.timer %preun -%service_del_preun suseconnect-keepalive.service suseconnect-keepalive.timer +%service_del_preun suseconnect-keepalive.service suseconnect-keepalive.timer suse-uptime-tracker.service suse-uptime-tracker.timer %postun -%service_del_postun suseconnect-keepalive.service suseconnect-keepalive.timer +%service_del_postun suseconnect-keepalive.service suseconnect-keepalive.timer suse-uptime-tracker.service suse-uptime-tracker.timer %posttrans if [ -e /run/suseconnect-keepalive.timer.is-enabled ]; then @@ -219,15 +224,19 @@ fi %license LICENSE LICENSE.LGPL %doc README.md %{_bindir}/suseconnect +%{_bindir}/suse-uptime-tracker %{_bindir}/SUSEConnect %{_sbindir}/SUSEConnect %{_sbindir}/rcsuseconnect-keepalive +%{_sbindir}/rcsuse-uptime-tracker /usr/lib/zypper/commands %{_mandir}/man8/* %{_mandir}/man5/* %{_unitdir}/suseconnect-keepalive.service %{_unitdir}/suseconnect-keepalive.timer %config %{_sysconfdir}/SUSEConnect.example +%{_unitdir}/suse-uptime-tracker.service +%{_unitdir}/suse-uptime-tracker.timer %files -n libsuseconnect %license LICENSE LICENSE.LGPL diff --git a/cmd/suse-uptime-tracker/uptimeTrackerUsage.txt b/cmd/suse-uptime-tracker/uptimeTrackerUsage.txt new file mode 100644 index 00000000..b86acff3 --- /dev/null +++ b/cmd/suse-uptime-tracker/uptimeTrackerUsage.txt @@ -0,0 +1,6 @@ +Usage: suse-uptime-tracker [options] +Keep track of system uptime. If no options are specified, it will update +the uptime tracking log file with the current uptime. + + --version Print program version. + -h, --help Show this message. diff --git a/cmd/suse-uptime-tracker/uptime_tracker.go b/cmd/suse-uptime-tracker/uptime_tracker.go new file mode 100644 index 00000000..be8168ab --- /dev/null +++ b/cmd/suse-uptime-tracker/uptime_tracker.go @@ -0,0 +1,152 @@ +package main + +import ( + "bufio" + _ "embed" + "errors" + "flag" + "fmt" + "os" + "sort" + "strings" + "time" +) + +var ( + //go:embed version.txt + version string + //go:embed uptimeTrackerUsage.txt + uptimeTrackerUsageText string +) + +const ( + uptimeCheckLogsFilePath = "/etc/zypp/suse-uptime.log" + dateStringFormat = "2006-01-02" + initUptimeHours = "000000000000000000000000" // initialize the uptime hours bit string with + daysBeforePurge = 90 // purge all the records after this many days +) + +func getShortenedVersion() string { + return strings.Split(strings.TrimSpace(version), "~")[0] +} + +func exitOnError(err error) { + if err == nil { + return + } + fmt.Println(err) + os.Exit(1) +} + +func displayUptimeVersion() { + var ( + version bool + ) + + flag.Usage = func() { + fmt.Print(uptimeTrackerUsageText) + } + + flag.BoolVar(&version, "version", false, "") + + flag.Parse() + if version { + fmt.Println(getShortenedVersion()) + os.Exit(0) + } +} + +func readUptimeLogFile(uptimeLogsFilePath string) (map[string]string, error) { + uptimeLogsFile, err := os.Open(uptimeLogsFilePath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // file doesn't exist, so don't error out + return nil, nil + } + return nil, err + } + fileScanner := bufio.NewScanner(uptimeLogsFile) + fileScanner.Split(bufio.ScanLines) + var logEntries = make(map[string]string) + + var entry []string + for fileScanner.Scan() { + entryText := fileScanner.Text() + entry = strings.Split(entryText, ":") + if len(entry) != 2 { + return nil, errors.New("Uptime log file is corrupted. Invalid log entry " + entryText) + } + logEntries[entry[0]] = entry[1] + } + err = uptimeLogsFile.Close() + if err != nil { + return nil, err + } + return logEntries, nil +} + +func purgeOldUptimeLog(uptimeLogs map[string]string) (map[string]string, error) { + now := time.Now().UTC() + purgeBefore := now.AddDate(0, 0, -daysBeforePurge) + var purgedLogs = make(map[string]string) + for day, uptimeHours := range uptimeLogs { + timestamp, err := time.Parse(dateStringFormat, day) + if err != nil { + return nil, err + } + if timestamp.After(purgeBefore) { + purgedLogs[day] = uptimeHours + } + } + return purgedLogs, nil +} + +func updateUptimeLog(uptimeLogs map[string]string) map[string]string { + // NOTE: we are standardizing timezone to UTC + now := time.Now().UTC() + day := now.Format(dateStringFormat) + hours, _, _ := now.Clock() + _, ok := uptimeLogs[day] + if !ok { + uptimeLogs[day] = initUptimeHours + } + uptimeHoursMap := []rune(uptimeLogs[day]) + uptimeHoursMap[hours] = '1' + uptimeLogs[day] = string(uptimeHoursMap) + + return uptimeLogs +} + +func writeUptimeLogsFile(uptimeLogsFilePath string, uptimeLogs map[string]string) error { + uptimeLogsFile, err := os.OpenFile(uptimeLogsFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer uptimeLogsFile.Close() + + // sort the keys + keys := make([]string, 0, len(uptimeLogs)) + for day := range uptimeLogs { + keys = append(keys, day) + } + sort.Strings(keys) + + for _, day := range keys { + _, err = uptimeLogsFile.WriteString(day + ":" + uptimeLogs[day] + "\n") + if err != nil { + return err + } + } + return nil +} + +func main() { + displayUptimeVersion() + uptimeLogs, err := readUptimeLogFile(uptimeCheckLogsFilePath) + exitOnError(err) + uptimeLogs, err = purgeOldUptimeLog(uptimeLogs) + exitOnError(err) + uptimeLogs = updateUptimeLog(uptimeLogs) + err = writeUptimeLogsFile(uptimeCheckLogsFilePath, uptimeLogs) + exitOnError(err) +} diff --git a/cmd/suse-uptime-tracker/uptime_tracker_test.go b/cmd/suse-uptime-tracker/uptime_tracker_test.go new file mode 100644 index 00000000..e6bcc27f --- /dev/null +++ b/cmd/suse-uptime-tracker/uptime_tracker_test.go @@ -0,0 +1,92 @@ +package main + +import ( + "io/ioutil" + "os" + "testing" + "time" + + "github.com/google/uuid" +) + +const ( + DDMMYYYY = "2006-01-02" +) + +func createTestUptimeLogFileWithContent(content string) (string, error) { + tempFile, err := ioutil.TempFile("", "testUptimeLog") + if err != nil { + return "", err + } + tempFilePath := tempFile.Name() + if _, err := tempFile.WriteString(content); err != nil { + os.Remove(tempFilePath) + return "", err + } + if err := tempFile.Close(); err != nil { + os.Remove(tempFilePath) + return "", err + } + + return tempFilePath, nil +} + +func TestUptimeLogFileDoesNotExist(t *testing.T) { + bogusUptimeLogsFilePath := uuid.New().String() + uptimeLog, err := readUptimeLogFile(bogusUptimeLogsFilePath) + if uptimeLog != nil || err != nil { + t.Fatalf("Expected err and uptimeLog to be nil if uptime log file does not exist") + } +} + +func TestCorruptedUptimeLog(t *testing.T) { + corruptedUptimeLog := `2024-01-18:000000000000001000110000 +2024-01-13000000000000000000010000` + tempFilePath, err := createTestUptimeLogFileWithContent(corruptedUptimeLog) + if err != nil { + t.Fatalf("Failed to create temp uptime log file for testing") + } + _, err = readUptimeLogFile(tempFilePath) + if err == nil { + t.Fatalf("Expected an error for corrupted uptime logs entry") + } + defer os.Remove(tempFilePath) +} + +func TestPurgeOldUptimeLog(t *testing.T) { + datetime := time.Now().UTC() + currdate := string((datetime.Format(DDMMYYYY))) + olddatetime := datetime.AddDate(-1, 0, 0) + olddate := string((olddatetime.Format(DDMMYYYY))) + PurgeOldUptimeLog := currdate + ":000000000000001000110000\n" + olddate + ":000000000000000000010000\n" + tempFilePath, err := createTestUptimeLogFileWithContent(PurgeOldUptimeLog) + if err != nil { + t.Fatalf("Failed to populate old uptime logs content for testing") + } + uptimelog, _ := readUptimeLogFile(tempFilePath) + purgelog, _ := purgeOldUptimeLog(uptimelog) + if len(purgelog) != 1 { + t.Fatalf("Failed to purge old uptime logs entry") + } + defer os.Remove(tempFilePath) +} + +func TestUpdateuptimeLog(t *testing.T) { + datetime := time.Now().UTC() + hour, _, _ := datetime.Clock() + strhour := rune(hour) + currdate := string((datetime.Format(DDMMYYYY))) + PopulateUptimeLog := currdate + ":000000000000000000000000\n" + tempFilePath, err := createTestUptimeLogFileWithContent(PopulateUptimeLog) + if err != nil { + t.Fatalf("Failed to populate uptime logs content for testing") + } + uptimelog, _ := readUptimeLogFile(tempFilePath) + uptimelog = updateUptimeLog(uptimelog) + timeupd := string(uptimelog[currdate]) + activehr := timeupd[strhour : strhour+1] + if activehr != "1" { + t.Fatalf("Failed to update uptime hour ") + } + defer os.Remove(tempFilePath) +} diff --git a/cmd/suse-uptime-tracker/version.txt b/cmd/suse-uptime-tracker/version.txt new file mode 100644 index 00000000..3eefcb9d --- /dev/null +++ b/cmd/suse-uptime-tracker/version.txt @@ -0,0 +1 @@ +1.0.0