commit
e57ad37808
@ -0,0 +1,5 @@
|
||||
snapd-extra-utils (1.0.0) UNRELEASED; urgency=medium
|
||||
|
||||
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
||||
|
||||
-- Simon Quigley <tsimonq2@ubuntu.com> Mon, 14 Oct 2024 13:51:43 -0500
|
@ -0,0 +1,30 @@
|
||||
Source: snapd-extra-utils
|
||||
Section: devel
|
||||
Priority: optional
|
||||
Maintainer: Simon Quigley <tsimonq2@ubuntu.com>
|
||||
Rules-Requires-Root: no
|
||||
Build-Depends: debhelper-compat (= 13),
|
||||
golang-github-snapcore-snapd-dev (>= 2.62),
|
||||
golang-go,
|
||||
golang-golang-x-crypto-dev,
|
||||
golang-gopkg-yaml.v3-dev,
|
||||
qt6-base-dev
|
||||
Standards-Version: 4.7.0
|
||||
#Vcs-Browser: https://salsa.debian.org/debian/snapd-extra-utils
|
||||
#Vcs-Git: https://salsa.debian.org/debian/snapd-extra-utils.git
|
||||
|
||||
Package: snapd-seed-glue
|
||||
Architecture: any
|
||||
Depends: snapd, ${misc:Depends}, ${shlibs:Depends}
|
||||
Description: Installer and pre-seed utilities for snapd
|
||||
Primarily used in Calamares, snapd-seed-glue updates snap seeds in a given
|
||||
directory, on a pre-booted system. It handles dependency resolution, delta
|
||||
downloads, and full snap downloads.
|
||||
|
||||
Package: snapd-installation-monitor
|
||||
Architecture: any
|
||||
Depends: snapd, ${misc:Depends}, ${shlibs:Depends}
|
||||
Description: First-boot snap install notification
|
||||
When many snaps are preseeded, on first boot the user may be confused if they
|
||||
can not open one of those snaps. This simple notification informs the user,
|
||||
disappears when it is complete, and usually is not shown again.
|
@ -0,0 +1,24 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: snapd-extra-utils
|
||||
Upstream-Contact: Simon Quigley <tsimonq2@ubuntu.com>
|
||||
|
||||
Files: *
|
||||
Copyright: 2024 Simon Quigley <tsimonq2@ubuntu.com>
|
||||
License: GPL-3.0+
|
||||
|
||||
License: GPL-3.0+
|
||||
This package is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this package. If not, see <https://www.gnu.org/licenses/>.
|
||||
Comment:
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
|
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/make -f
|
||||
export DH_VERBOSE = 1
|
||||
export GO111MODULE = off
|
||||
export GOPATH=/usr/share/gocode
|
||||
export GOCACHE=$(CURDIR)/.gocache
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_build:
|
||||
(cd snapd-seed-glue && go build -gcflags="all=-N -l" -ldflags="-compressdwarf=false" -o snapd-seed-glue)
|
||||
(cd snapd-installation-monitor && cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo . && make)
|
@ -0,0 +1,2 @@
|
||||
snapd-installation-monitor/snapd-installation-monitor /usr/libexec
|
||||
snapd-installation-monitor/snapd-installation-monitor.desktop /etc/xdg/autostart
|
@ -0,0 +1 @@
|
||||
snapd-seed-glue/snapd-seed-glue /usr/bin/
|
@ -0,0 +1 @@
|
||||
3.0 (native)
|
@ -0,0 +1,65 @@
|
||||
cmake_minimum_required(VERSION 3.16...3.21)
|
||||
|
||||
# These are part of the public API. Projects should use them to provide a
|
||||
# consistent set of prefix-relative destinations.
|
||||
if(NOT QT_DEPLOY_BIN_DIR)
|
||||
set(QT_DEPLOY_BIN_DIR "bin")
|
||||
endif()
|
||||
if(NOT QT_DEPLOY_LIB_DIR)
|
||||
set(QT_DEPLOY_LIB_DIR "lib")
|
||||
endif()
|
||||
if(NOT QT_DEPLOY_PLUGINS_DIR)
|
||||
set(QT_DEPLOY_PLUGINS_DIR "plugins")
|
||||
endif()
|
||||
if(NOT QT_DEPLOY_QML_DIR)
|
||||
set(QT_DEPLOY_QML_DIR "qml")
|
||||
endif()
|
||||
if(NOT QT_DEPLOY_TRANSLATIONS_DIR)
|
||||
set(QT_DEPLOY_TRANSLATIONS_DIR "translations")
|
||||
endif()
|
||||
if(NOT QT_DEPLOY_PREFIX)
|
||||
set(QT_DEPLOY_PREFIX "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}")
|
||||
endif()
|
||||
if(QT_DEPLOY_PREFIX STREQUAL "")
|
||||
set(QT_DEPLOY_PREFIX .)
|
||||
endif()
|
||||
if(NOT QT_DEPLOY_IGNORED_LIB_DIRS)
|
||||
set(QT_DEPLOY_IGNORED_LIB_DIRS "/lib/x86_64-linux-gnu;/lib")
|
||||
endif()
|
||||
|
||||
# These are internal implementation details. They may be removed at any time.
|
||||
set(__QT_DEPLOY_SYSTEM_NAME "Linux")
|
||||
set(__QT_DEPLOY_IS_SHARED_LIBS_BUILD "ON")
|
||||
set(__QT_DEPLOY_TOOL "GRD")
|
||||
set(__QT_DEPLOY_IMPL_DIR "/home/tsimonq2/Code/25.04/snapd-extra-utils/snap-installation-monitor/.qt")
|
||||
set(__QT_DEPLOY_VERBOSE "")
|
||||
set(__QT_CMAKE_EXPORT_NAMESPACE "Qt6")
|
||||
set(__QT_DEPLOY_GENERATOR_IS_MULTI_CONFIG "0")
|
||||
set(__QT_DEPLOY_ACTIVE_CONFIG "Release")
|
||||
set(__QT_NO_CREATE_VERSIONLESS_FUNCTIONS "")
|
||||
set(__QT_DEFAULT_MAJOR_VERSION "6")
|
||||
set(__QT_DEPLOY_QT_ADDITIONAL_PACKAGES_PREFIX_PATH "")
|
||||
set(__QT_DEPLOY_QT_INSTALL_PREFIX "/usr")
|
||||
set(__QT_DEPLOY_QT_INSTALL_BINS "lib/qt6/bin")
|
||||
set(__QT_DEPLOY_QT_INSTALL_DATA "share/qt6")
|
||||
set(__QT_DEPLOY_QT_INSTALL_LIBEXECS "lib/qt6/libexec")
|
||||
set(__QT_DEPLOY_QT_INSTALL_PLUGINS "lib/x86_64-linux-gnu/qt6/plugins")
|
||||
set(__QT_DEPLOY_QT_INSTALL_TRANSLATIONS "share/qt6/translations")
|
||||
set(__QT_DEPLOY_TARGET_QT_PATHS_PATH "/usr/lib/qt6/bin/qtpaths")
|
||||
set(__QT_DEPLOY_PLUGINS "")
|
||||
set(__QT_DEPLOY_MUST_ADJUST_PLUGINS_RPATH "ON")
|
||||
set(__QT_DEPLOY_USE_PATCHELF "")
|
||||
set(__QT_DEPLOY_PATCHELF_EXECUTABLE "")
|
||||
set(__QT_DEPLOY_QT_IS_MULTI_CONFIG_BUILD_WITH_DEBUG "FALSE")
|
||||
set(__QT_DEPLOY_QT_DEBUG_POSTFIX "")
|
||||
|
||||
# Define the CMake commands to be made available during deployment.
|
||||
set(__qt_deploy_support_files
|
||||
"/usr/lib/x86_64-linux-gnu/cmake/Qt6Core/Qt6CoreDeploySupport.cmake"
|
||||
)
|
||||
foreach(__qt_deploy_support_file IN LISTS __qt_deploy_support_files)
|
||||
include("${__qt_deploy_support_file}")
|
||||
endforeach()
|
||||
|
||||
unset(__qt_deploy_support_file)
|
||||
unset(__qt_deploy_support_files)
|
@ -0,0 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.5.0)
|
||||
project(snapd-installation-monitor)
|
||||
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
find_package(Qt6 COMPONENTS Widgets DBus REQUIRED)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
add_executable(snapd-installation-monitor main.cpp)
|
||||
target_link_libraries(snapd-installation-monitor Qt6::Widgets Qt6::DBus)
|
@ -0,0 +1,76 @@
|
||||
// Copyright (C) 2024 Simon Quigley <tsimonq2@ubuntu.com>
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 3
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
#include <QApplication>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QtDBus/QtDBus>
|
||||
#include <QTimer>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// Create and set application icon
|
||||
QIcon appIcon = QIcon::fromTheme("dialog-information");
|
||||
QApplication::setWindowIcon(appIcon);
|
||||
|
||||
// DBus interface to systemd
|
||||
QDBusInterface systemd("org.freedesktop.systemd1",
|
||||
"/org/freedesktop/systemd1",
|
||||
"org.freedesktop.systemd1.Manager",
|
||||
QDBusConnection::systemBus());
|
||||
|
||||
// Retrieve current status of snapd.seeded.service, which tracks the preseed process
|
||||
QDBusMessage methodCall = systemd.call("GetUnit", "snapd.seeded.service");
|
||||
QDBusObjectPath unitPath = methodCall.arguments().at(0).value<QDBusObjectPath>();
|
||||
QDBusInterface unit("org.freedesktop.systemd1",
|
||||
unitPath.path(),
|
||||
"org.freedesktop.systemd1.Unit",
|
||||
QDBusConnection::systemBus());
|
||||
QVariant activeState = unit.property("ActiveState");
|
||||
QVariant subState = unit.property("SubState");
|
||||
|
||||
// System tray icon setup
|
||||
QSystemTrayIcon trayIcon(appIcon);
|
||||
trayIcon.setToolTip("Snap Installation Monitor");
|
||||
|
||||
// Initial message displayed in the system tray
|
||||
auto showMessage = [&trayIcon]() {
|
||||
trayIcon.showMessage("Installation Notice", "Finalizing installation of snaps, please wait...",
|
||||
QSystemTrayIcon::Information, 15000);
|
||||
};
|
||||
|
||||
// If the user clicks the system tray icon, display the notification again
|
||||
QObject::connect(&trayIcon, &QSystemTrayIcon::activated, [&](QSystemTrayIcon::ActivationReason reason) {
|
||||
if (reason == QSystemTrayIcon::Trigger) {
|
||||
showMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// Exit immediately if the service is "active (exited)", launch the GUI parts otherwise
|
||||
if (activeState.toString() == "active" && subState.toString() == "exited") { return 0; }
|
||||
trayIcon.show();
|
||||
showMessage();
|
||||
|
||||
QTimer timer;
|
||||
QObject::connect(&timer, &QTimer::timeout, [&unit, &trayIcon]() {
|
||||
QVariant newState = unit.property("ActiveState");
|
||||
QVariant newSubState = unit.property("SubState");
|
||||
if (newState.toString() == "active" && newSubState.toString() == "exited") {
|
||||
trayIcon.hide();
|
||||
QApplication::quit();
|
||||
}
|
||||
});
|
||||
|
||||
timer.start(5000); // Check every 5 seconds
|
||||
|
||||
return app.exec();
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Snap Installation Monitor
|
||||
Exec=/usr/libexec/snapd-installation-monitor
|
||||
Icon=dialog-information
|
||||
Comment=Monitor snapd seeding at startup
|
||||
X-LXQt-Need-Tray=true
|
||||
NoDisplay=true
|
@ -0,0 +1,225 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/snapcore/snapd/asserts"
|
||||
"github.com/snapcore/snapd/snap"
|
||||
"github.com/snapcore/snapd/store"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// downloadAssertions dynamically fetches the necessary assertions and saves them to a file.
|
||||
// It ensures that the account-key assertion is written first in the .assert file.
|
||||
func downloadAssertions(storeClient *store.Store, snapInfo *snap.Info, downloadDir string) error {
|
||||
// Define the path for the assertions file
|
||||
assertionsPath := filepath.Join(downloadDir, fmt.Sprintf("%s_%d.assert", snapInfo.SuggestedName, snapInfo.Revision.N))
|
||||
|
||||
// Extract necessary fields from snapInfo
|
||||
snapSHA := snapInfo.Sha3_384
|
||||
snapID := snapInfo.SnapID
|
||||
publisherID := snapInfo.Publisher.ID
|
||||
series := "16" // Consider making this dynamic if possible
|
||||
|
||||
// Define assertion types
|
||||
assertionTypes := map[string]*asserts.AssertionType{
|
||||
"snap-revision": asserts.SnapRevisionType,
|
||||
"snap-declaration": asserts.SnapDeclarationType,
|
||||
"account-key": asserts.AccountKeyType,
|
||||
"account": asserts.AccountType,
|
||||
}
|
||||
|
||||
// Open the assertions file for writing
|
||||
assertionsFile, err := os.Create(assertionsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create assertions file: %w", err)
|
||||
}
|
||||
defer assertionsFile.Close()
|
||||
|
||||
// Step 1: Fetch snap-declaration assertion
|
||||
snapDecl, err := storeClient.Assertion(assertionTypes["snap-declaration"], []string{series, snapID}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch snap-declaration assertion for snap %s: %w", snapInfo.SuggestedName, err)
|
||||
}
|
||||
|
||||
// Step 2: Extract sign-key-sha3-384 from snap-declaration
|
||||
signKey, ok := snapDecl.Header("sign-key-sha3-384").(string)
|
||||
if !ok || signKey == "" {
|
||||
return fmt.Errorf("snap-declaration assertion missing 'sign-key-sha3-384' header for snap %s", snapInfo.SuggestedName)
|
||||
}
|
||||
|
||||
// Step 3: Fetch account-key assertion using sign-key-sha3-384 (no decoding)
|
||||
accountKeyAssertion, err := storeClient.Assertion(assertionTypes["account-key"], []string{signKey}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch account-key assertion for snap %s: %w", snapInfo.SuggestedName, err)
|
||||
}
|
||||
|
||||
// Step 4: Fetch account assertion using publisher-id
|
||||
accountAssertion, err := storeClient.Assertion(assertionTypes["account"], []string{publisherID}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch account assertion for snap %s: %w", snapInfo.SuggestedName, err)
|
||||
}
|
||||
|
||||
// Step 5: Fetch snap-revision assertion
|
||||
snapSHA384Bytes, err := hex.DecodeString(snapSHA)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding SHA3-384 hex string for snap %s: %w", snapInfo.SuggestedName, err)
|
||||
}
|
||||
snapSHA384Base64 := base64.RawURLEncoding.EncodeToString(snapSHA384Bytes)
|
||||
//revisionKey := fmt.Sprintf("%s/global-upload", snapSHA384Base64)
|
||||
revisionKey := fmt.Sprintf("%s/", snapSHA384Base64)
|
||||
|
||||
snapRevisionAssertion, err := storeClient.Assertion(assertionTypes["snap-revision"], []string{revisionKey}, nil)
|
||||
if err != nil {
|
||||
verboseLog("Failed to fetch snap-revision assertion for snap %s: %v", snapInfo.SuggestedName, err)
|
||||
// Proceeding without snap-revision might be acceptable based on your use-case
|
||||
}
|
||||
|
||||
// Step 6: Write assertions in the desired order
|
||||
// 1. account-key
|
||||
writeAssertion("account-key", accountKeyAssertion, assertionsFile)
|
||||
|
||||
// 2. account
|
||||
writeAssertion("account", accountAssertion, assertionsFile)
|
||||
|
||||
// 3. snap-declaration
|
||||
writeAssertion("snap-declaration", snapDecl, assertionsFile)
|
||||
|
||||
// 4. snap-revision (if fetched successfully)
|
||||
if snapRevisionAssertion != nil {
|
||||
writeAssertion("snap-revision", snapRevisionAssertion, assertionsFile)
|
||||
}
|
||||
|
||||
verboseLog("Assertions downloaded and saved to: %s", assertionsPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAssertion(assertionType string, assertion asserts.Assertion, file *os.File) {
|
||||
fieldOrder := map[string][]string{
|
||||
"account-key": {
|
||||
"type", "authority-id", "revision", "public-key-sha3-384",
|
||||
"account-id", "name", "since", "body-length", "sign-key-sha3-384",
|
||||
},
|
||||
"account": {
|
||||
"type", "authority-id", "revision", "account-id", "display-name",
|
||||
"timestamp", "username", "validation", "sign-key-sha3-384",
|
||||
},
|
||||
"snap-declaration": {
|
||||
"type", "format", "authority-id", "revision", "series", "snap-id",
|
||||
"aliases", "auto-aliases", "plugs", "publisher-id", "slots",
|
||||
"snap-name", "timestamp", "sign-key-sha3-384",
|
||||
},
|
||||
"snap-revision": {
|
||||
"type", "authority-id", "snap-sha3-384", "developer-id",
|
||||
"provenance", "snap-id", "snap-revision", "snap-size",
|
||||
"timestamp", "sign-key-sha3-384",
|
||||
},
|
||||
}
|
||||
|
||||
body := assertion.Body()
|
||||
bodyLength := len(body)
|
||||
headers := assertion.Headers()
|
||||
|
||||
// Only write the account assertion if it is not Canonical
|
||||
if assertionType == "account" {
|
||||
value, exists := headers["username"]
|
||||
if exists && value == "canonical" {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// provenance seems to be a field only available in newer snap revisions
|
||||
// For snaps published in 2023 or earlier, do not include this field
|
||||
timestamp, exists := headers["timestamp"]
|
||||
if assertionType == "snap-revision" && exists {
|
||||
layout := time.RFC3339
|
||||
parsedTime, _ := time.Parse(layout, timestamp.(string))
|
||||
thresholdTime := time.Date(2023, time.December, 9, 0, 0, 0, 0, time.UTC)
|
||||
if parsedTime.Before(thresholdTime) || parsedTime.Equal(thresholdTime) {
|
||||
delete(headers, "provenance")
|
||||
}
|
||||
}
|
||||
|
||||
// Write headers in the specified order
|
||||
for _, key := range fieldOrder[assertionType] {
|
||||
value, exists := headers[key]
|
||||
if !exists || value == "" {
|
||||
continue
|
||||
}
|
||||
if key == "type" {
|
||||
fmt.Fprintf(file, "%s: %s\n", key, assertionType)
|
||||
} else if key == "body-length" && bodyLength > 0 {
|
||||
file.WriteString(fmt.Sprintf("body-length: %d\n", bodyLength))
|
||||
continue
|
||||
} else if isComplexField(key) {
|
||||
fmt.Fprintf(file, "%s:\n", key)
|
||||
serializeComplexField(value, file)
|
||||
} else {
|
||||
fmt.Fprintf(file, "%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
file.WriteString("\n")
|
||||
|
||||
// Write the body if it exists
|
||||
if bodyLength > 0 {
|
||||
file.Write(body)
|
||||
file.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// Write the signature
|
||||
_, signature := assertion.Signature()
|
||||
file.Write(signature)
|
||||
if assertionType != "snap-revision" {
|
||||
file.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func serializeComplexField(value interface{}, file *os.File) {
|
||||
var buf strings.Builder
|
||||
encoder := yaml.NewEncoder(&buf)
|
||||
encoder.SetIndent(2)
|
||||
defer encoder.Close()
|
||||
|
||||
// Encode the value directly
|
||||
if err := encoder.Encode(value); err != nil {
|
||||
log.Fatalf("Error encoding YAML: %v", err)
|
||||
}
|
||||
|
||||
// Write the serialized YAML to the file with proper indentation
|
||||
lines := strings.Split(buf.String(), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
line = strings.ReplaceAll(line, `"true"`, "true")
|
||||
line = strings.ReplaceAll(line, `"false"`, "false")
|
||||
line = strings.ReplaceAll(line, `'*'`, "*")
|
||||
// Check for dashes indicating list items
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "-") && strings.Contains(line, ":") {
|
||||
before, after, found := strings.Cut(line, "- ")
|
||||
if found {
|
||||
file.WriteString(fmt.Sprintf(" %s-\n", before))
|
||||
if after != "" {
|
||||
file.WriteString(fmt.Sprintf(" %s%s\n", before, after))
|
||||
}
|
||||
} else {
|
||||
file.WriteString(fmt.Sprintf(" %s-\n", line))
|
||||
}
|
||||
} else if strings.TrimSpace(line) != "" {
|
||||
// For any other non-empty lines, indent them correctly
|
||||
file.WriteString(fmt.Sprintf(" %s\n", line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isComplexField checks if a field is complex (nested) in YAML
|
||||
func isComplexField(key string) bool {
|
||||
return key == "aliases" || key == "auto-aliases" || key == "plugs" || key == "slots" || key == "allow-installation" || key == "allow-connection"
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/snapcore/snapd/store"
|
||||
)
|
||||
|
||||
// cleanUpFiles removes partial, old, and orphaned snap and assertion files from the download and assertions directories.
|
||||
func cleanUpFiles(snapsDir, assertionsDir, seedYaml string) {
|
||||
verboseLog("Starting cleanup process...")
|
||||
|
||||
// Load the seed.yaml data
|
||||
seedData := loadSeedData(seedYaml)
|
||||
|
||||
// Create a map of valid snap and assertion files based on seed.yaml
|
||||
validSnaps := make(map[string]bool)
|
||||
validAssertions := make(map[string]bool)
|
||||
|
||||
// Populate valid snaps and assertions from the seed.yaml data
|
||||
for _, snap := range seedData.Snaps {
|
||||
// Ensure correct extraction of revision
|
||||
revision := extractRevisionFromFile(snap.File)
|
||||
if revision == "" {
|
||||
verboseLog("Failed to extract revision from file name: %s", snap.File)
|
||||
continue
|
||||
}
|
||||
snapFileName := fmt.Sprintf("%s_%s.snap", snap.Name, revision)
|
||||
assertionFileName := fmt.Sprintf("%s_%s.assert", snap.Name, revision)
|
||||
|
||||
validSnaps[snapFileName] = true
|
||||
validAssertions[assertionFileName] = true
|
||||
}
|
||||
|
||||
// Log valid snaps and assertions
|
||||
verboseLog("Valid Snaps: %v", validSnaps)
|
||||
verboseLog("Valid Assertions: %v", validAssertions)
|
||||
|
||||
// Remove outdated or partial snap files
|
||||
files, err := os.ReadDir(snapsDir)
|
||||
if err != nil {
|
||||
verboseLog("Error reading snaps directory for cleanup: %v", err)
|
||||
} else {
|
||||
for _, file := range files {
|
||||
filePath := filepath.Join(snapsDir, file.Name())
|
||||
if strings.HasSuffix(file.Name(), ".partial") || strings.HasSuffix(file.Name(), ".delta") {
|
||||
verboseLog("Removing partial/delta file: %s\n", filePath)
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
verboseLog("Failed to remove file %s: %v", filePath, err)
|
||||
} else if verbose {
|
||||
verboseLog("Removed partial/delta file: %s", filePath)
|
||||
}
|
||||
} else if strings.HasSuffix(file.Name(), ".snap") {
|
||||
if !validSnaps[file.Name()] {
|
||||
verboseLog("Removing outdated or orphaned snap file: %s\n", filePath)
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
verboseLog("Failed to remove snap file %s: %v", filePath, err)
|
||||
} else if verbose {
|
||||
verboseLog("Removed snap file: %s", filePath)
|
||||
}
|
||||
} else {
|
||||
verboseLog("Snap file %s is valid and retained.\n", file.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove orphaned assertion files
|
||||
files, err = os.ReadDir(assertionsDir)
|
||||
if err != nil {
|
||||
verboseLog("Error reading assertions directory for cleanup: %v", err)
|
||||
} else {
|
||||
for _, file := range files {
|
||||
filePath := filepath.Join(assertionsDir, file.Name())
|
||||
if strings.HasSuffix(file.Name(), ".assert") {
|
||||
if !validAssertions[file.Name()] {
|
||||
verboseLog("Removing orphaned assertion file: %s\n", filePath)
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
verboseLog("Failed to remove assertion file %s: %v", filePath, err)
|
||||
} else if verbose {
|
||||
verboseLog("Removed assertion file: %s", filePath)
|
||||
}
|
||||
} else {
|
||||
verboseLog("Assertion file %s is valid and retained.\n", file.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verboseLog("Cleanup process completed.")
|
||||
}
|
||||
|
||||
// removeOrphanedFiles deletes the assertion and snap file corresponding to the removed snap.
|
||||
func removeOrphanedFiles(snapName string, revision int, assertionsDir string, snapsDir string) {
|
||||
assertionFilePath := filepath.Join(assertionsDir, fmt.Sprintf("%s_%d.assert", snapName, revision))
|
||||
snapFilePath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d.snap", snapName, revision))
|
||||
if fileExists(assertionFilePath) {
|
||||
err := os.Remove(assertionFilePath)
|
||||
if err != nil {
|
||||
verboseLog("Failed to remove assertion file %s: %v", assertionFilePath, err)
|
||||
} else {
|
||||
verboseLog("Removed assertion file: %s", assertionFilePath)
|
||||
}
|
||||
} else {
|
||||
verboseLog("Assertion file %s does not exist. No action taken.", assertionFilePath)
|
||||
}
|
||||
if fileExists(snapFilePath) {
|
||||
err := os.Remove(snapFilePath)
|
||||
if err != nil {
|
||||
verboseLog("Failed to remove snap file %s: %v", snapFilePath, err)
|
||||
} else {
|
||||
verboseLog("Removed snap file: %s", snapFilePath)
|
||||
}
|
||||
} else {
|
||||
verboseLog("Snap file %s does not exist. No action taken.", snapFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanUpCurrentSnaps removes snaps from currentSnaps that are not marked as required.
|
||||
func cleanUpCurrentSnaps(assertionsDir string, snapsDir string) {
|
||||
var filteredSnaps []*store.CurrentSnap
|
||||
|
||||
for _, snap := range currentSnaps {
|
||||
if requiredSnaps[snap.InstanceName] {
|
||||
filteredSnaps = append(filteredSnaps, snap)
|
||||
} else {
|
||||
verboseLog("Removing unnecessary snap: %s\n", snap.InstanceName)
|
||||
removeOrphanedFiles(snap.InstanceName, snap.Revision.N, assertionsDir, snapsDir)
|
||||
}
|
||||
}
|
||||
currentSnaps = filteredSnaps
|
||||
|
||||
// Log the updated currentSnaps
|
||||
verboseLog("Filtered currentSnaps after cleanup:")
|
||||
for _, snap := range currentSnaps {
|
||||
verboseLog("- %s_%d.snap", snap.InstanceName, snap.Revision.N)
|
||||
}
|
||||
}
|
||||
|
||||
// removeStateJson removes the state.json file if it exists
|
||||
func removeStateJson(stateJsonPath string) {
|
||||
if _, err := os.Stat(stateJsonPath); err == nil {
|
||||
if err := os.Remove(stateJsonPath); err != nil {
|
||||
verboseLog("Failed to remove state.json: %v", err)
|
||||
} else if verbose {
|
||||
verboseLog("Removed state.json at %s", stateJsonPath)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/snapcore/snapd/snap"
|
||||
"github.com/snapcore/snapd/store"
|
||||
)
|
||||
|
||||
// downloadSnap downloads a snap file with retry logic
|
||||
func downloadSnap(storeClient *store.Store, snapInfo *snap.Info, downloadPath string) error {
|
||||
downloadInfo := &snap.DownloadInfo{
|
||||
DownloadURL: snapInfo.DownloadURL,
|
||||
}
|
||||
|
||||
pbar := NewProgressMeter(snapInfo.SuggestedName, snapInfo.Version, false)
|
||||
progressTracker.UpdateStepProgress(0)
|
||||
|
||||
for attempts := 1; attempts <= 5; attempts++ {
|
||||
verboseLog("Attempt %d to download snap: %s", attempts, downloadPath)
|
||||
err := storeClient.Download(ctx, snapInfo.SnapID, downloadPath, downloadInfo, pbar, nil, nil)
|
||||
if err == nil {
|
||||
pbar.Finished()
|
||||
return nil // Successful download
|
||||
}
|
||||
if verbose {
|
||||
verboseLog("Attempt %d to download %s failed: %v", attempts, snapInfo.SuggestedName, err)
|
||||
} else if progressTracker != nil {
|
||||
progressTracker.UpdateStepProgress(0)
|
||||
}
|
||||
if attempts == 5 {
|
||||
return fmt.Errorf("snap download failed after 5 attempts: %v", err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("snap download failed after 5 attempts")
|
||||
}
|
||||
|
||||
// downloadSnapDeltaWithRetries downloads the delta file with retry logic and exponential backoff.
|
||||
func downloadSnapDeltaWithRetries(storeClient *store.Store, delta *snap.DeltaInfo, result *store.SnapActionResult, deltaPath string, maxRetries int, snapName string) error {
|
||||
if !verbose {
|
||||
verboseLog("Downloading delta for %s", snapName)
|
||||
}
|
||||
var lastErr error
|
||||
backoff := 1 * time.Second
|
||||
|
||||
for attempts := 1; attempts <= maxRetries; attempts++ {
|
||||
verboseLog("Attempt %d to download delta: %s", attempts, deltaPath)
|
||||
err := downloadSnapDelta(storeClient, delta, result, deltaPath)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
verboseLog("Attempt %d to download delta failed: %v", attempts, err)
|
||||
time.Sleep(backoff)
|
||||
backoff *= 2
|
||||
}
|
||||
return fmt.Errorf("delta download failed after %d attempts: %v", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
// downloadSnapDelta downloads the delta file.
|
||||
func downloadSnapDelta(storeClient *store.Store, delta *snap.DeltaInfo, result *store.SnapActionResult, deltaPath string) error {
|
||||
verboseLog("Downloading delta from revision %d to %d from: %s", delta.FromRevision, delta.ToRevision, delta.DownloadURL)
|
||||
|
||||
downloadInfo := &snap.DownloadInfo{
|
||||
DownloadURL: delta.DownloadURL,
|
||||
Size: delta.Size,
|
||||
Sha3_384: delta.Sha3_384,
|
||||
}
|
||||
|
||||
// Use the SnapID from the associated SnapActionResult's Info
|
||||
snapID := result.Info.SnapID
|
||||
|
||||
pbar := NewProgressMeter(result.Info.SuggestedName, result.Info.Version, true)
|
||||
progressTracker.UpdateStepProgress(0)
|
||||
|
||||
// Download the delta file
|
||||
if err := storeClient.Download(ctx, snapID, deltaPath, downloadInfo, pbar, nil, nil); err != nil {
|
||||
progressTracker.UpdateStepProgress(0)
|
||||
return fmt.Errorf("delta download failed: %v", err)
|
||||
}
|
||||
verboseLog("Downloaded %s to %s", delta.DownloadURL, deltaPath)
|
||||
pbar.Finished()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadAndApplySnap handles the downloading and delta application process.
|
||||
// It returns the snap information and an error if any.
|
||||
func downloadAndApplySnap(storeClient *store.Store, result *store.SnapActionResult, snapsDir, assertionsDir string, currentSnap *store.CurrentSnap) (*snap.Info, error) {
|
||||
if result == nil || result.Info == nil {
|
||||
verboseLog("No updates available for snap. Skipping download and assertions.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
snapInfo := result.Info
|
||||
downloadPath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d.snap", snapInfo.SuggestedName, snapInfo.Revision.N))
|
||||
|
||||
// Check if a delta can be applied
|
||||
if currentSnap != nil && len(result.Deltas) > 0 {
|
||||
for _, delta := range result.Deltas {
|
||||
deltaPath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d_to_%d.delta", snapInfo.SuggestedName, delta.FromRevision, delta.ToRevision))
|
||||
if err := downloadSnapDeltaWithRetries(storeClient, &delta, result, deltaPath, 5, snapInfo.SuggestedName); err == nil {
|
||||
oldSnapPath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d.snap", snapInfo.SuggestedName, delta.FromRevision))
|
||||
if fileExists(oldSnapPath) {
|
||||
if err := applyDelta(oldSnapPath, deltaPath, downloadPath); err == nil {
|
||||
verboseLog("Delta applied successfully for snap %s", snapInfo.SuggestedName)
|
||||
// Download assertions after successful snap download
|
||||
if err := downloadAssertions(storeClient, snapInfo, assertionsDir); err != nil {
|
||||
return nil, fmt.Errorf("failed to download assertions for snap %s: %w", snapInfo.SuggestedName, err)
|
||||
}
|
||||
return snapInfo, nil // Successful delta application
|
||||
} else {
|
||||
verboseLog("Failed to apply delta for snap %s: %v", snapInfo.SuggestedName, err)
|
||||
}
|
||||
} else {
|
||||
verboseLog("Old snap file %s does not exist. Cannot apply delta.", oldSnapPath)
|
||||
}
|
||||
} else {
|
||||
verboseLog("Attempt to download delta for snap %s failed: %v", snapInfo.SuggestedName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no delta was applied or no deltas are available, fallback to downloading the full snap
|
||||
if err := downloadSnap(storeClient, snapInfo, downloadPath); err != nil {
|
||||
return nil, fmt.Errorf("failed to download snap %s: %w", snapInfo.SuggestedName, err)
|
||||
}
|
||||
|
||||
// Download assertions after successful snap download
|
||||
if err := downloadAssertions(storeClient, snapInfo, assertionsDir); err != nil {
|
||||
return nil, fmt.Errorf("failed to download assertions for snap %s: %w", snapInfo.SuggestedName, err)
|
||||
}
|
||||
|
||||
verboseLog("Downloaded and applied snap: %s, revision: %d", snapInfo.SuggestedName, snapInfo.Revision.N)
|
||||
return snapInfo, nil
|
||||
}
|
||||
|
||||
// applyDelta applies the downloaded delta using xdelta3.
|
||||
func applyDelta(oldSnapPath, deltaPath, newSnapPath string) error {
|
||||
verboseLog("Applying delta from %s to %s using %s", oldSnapPath, newSnapPath, deltaPath)
|
||||
|
||||
cmd := exec.Command("xdelta3", "-d", "-s", oldSnapPath, deltaPath, newSnapPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
verboseLog("xdelta3 output: %s", string(output))
|
||||
return fmt.Errorf("failed to apply delta: %v - %s", err, string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
module snapd-seed-glue
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/snapcore/snapd v0.0.0-20241012091728-e440fb944764
|
||||
golang.org/x/crypto v0.28.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/canonical/go-efilib v0.4.0 // indirect
|
||||
github.com/canonical/go-sp800.108-kdf v0.0.0-20210314145419-a3359f2d21b9 // indirect
|
||||
github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 // indirect
|
||||
github.com/canonical/go-tpm2 v0.0.0-20210827151749-f80ff5afff61 // indirect
|
||||
github.com/canonical/tcglog-parser v0.0.0-20210824131805-69fa1e9f0ad2 // indirect
|
||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
|
||||
github.com/juju/ratelimit v1.0.1 // indirect
|
||||
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 // indirect
|
||||
github.com/snapcore/secboot v0.0.0-20240411101434-f3ad7c92552a // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/term v0.25.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
gopkg.in/macaroon.v1 v1.0.0-20150121114231-ab3940c6c165 // indirect
|
||||
gopkg.in/retry.v1 v1.0.3 // indirect
|
||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066 // indirect
|
||||
)
|
@ -0,0 +1,44 @@
|
||||
github.com/canonical/go-efilib v0.4.0 h1:2ee5pvhIZ+g1EO4HxFE/owBgs5Up2g7dw1+Ls9/fiSs=
|
||||
github.com/canonical/go-efilib v0.4.0/go.mod h1:9b2PNAuPcZsB76x75/uwH99D8CyH/A2y4rq1/+bvplg=
|
||||
github.com/canonical/go-sp800.108-kdf v0.0.0-20210314145419-a3359f2d21b9 h1:USzKjrfWo/ESzozv2i3OMM7XDgxrZRvaHFrKkIKRtwU=
|
||||
github.com/canonical/go-sp800.108-kdf v0.0.0-20210314145419-a3359f2d21b9/go.mod h1:Zrs3YjJr+w51u0R/dyLh/oWt/EcBVdLPCVFYC4daW5s=
|
||||
github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw=
|
||||
github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM=
|
||||
github.com/canonical/go-tpm2 v0.0.0-20210827151749-f80ff5afff61 h1:DsyeCtFXqOdukmhPOunohjSlyxDHTqWSW1O4rD9N3L8=
|
||||
github.com/canonical/go-tpm2 v0.0.0-20210827151749-f80ff5afff61/go.mod h1:vG41hdbBjV4+/fkubTT1ENBBqSkLwLr7mCeW9Y6kpZY=
|
||||
github.com/canonical/tcglog-parser v0.0.0-20210824131805-69fa1e9f0ad2 h1:CbwVq64ruNLx/S3XA0LO6QMsw6Vc2inK+RcS6D2c4Ns=
|
||||
github.com/canonical/tcglog-parser v0.0.0-20210824131805-69fa1e9f0ad2/go.mod h1:QoW2apR2tBl6T/4czdND/EHjL1Ia9cCmQnIj9Xe0Kt8=
|
||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
|
||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
|
||||
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 h1:PaunR+BhraKSLxt2awQ42zofkP+NKh/VjQ0PjIMk/y4=
|
||||
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785/go.mod h1:D3SsWAXK7wCCBZu+Vk5hc1EuKj/L3XN1puEMXTU4LrQ=
|
||||
github.com/snapcore/secboot v0.0.0-20240411101434-f3ad7c92552a h1:yzzVi0yUosDYkjSQqGZNVtaVi+6yNFLiF0erKHlBbdo=
|
||||
github.com/snapcore/secboot v0.0.0-20240411101434-f3ad7c92552a/go.mod h1:72paVOkm4sJugXt+v9ItmnjXgO921D8xqsbH2OekouY=
|
||||
github.com/snapcore/snapd v0.0.0-20241012091728-e440fb944764 h1:RBbGB8WMdKgB7FCPXuUIBns8L+LezNsbvYNYXrG752w=
|
||||
github.com/snapcore/snapd v0.0.0-20241012091728-e440fb944764/go.mod h1:c9j4npxTEGjMy6E/7ocAazCMR+82ZWTh/o+5Ic7ilPc=
|
||||
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M=
|
||||
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
gopkg.in/macaroon.v1 v1.0.0-20150121114231-ab3940c6c165 h1:85xqOSyTpSzplW7fyO9bOZpSsemJc9UKzEQR2L4k32k=
|
||||
gopkg.in/macaroon.v1 v1.0.0-20150121114231-ab3940c6c165/go.mod h1:PABpHZvxAbIuSYTPWJdQsNu0mtx+HX/1NIm3IT95IX0=
|
||||
gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs=
|
||||
gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g=
|
||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
|
||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066 h1:UrD21H1Ue5Nl8f2x/NQJBRdc49YGmla3mRStinH8CCE=
|
||||
maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066/go.mod h1:DEvumi+swYmlKxSlnsvPwS15tRjoypCCeJFXswU5FfQ=
|
@ -0,0 +1,202 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/snapcore/snapd/snap"
|
||||
"github.com/snapcore/snapd/store"
|
||||
)
|
||||
|
||||
// Seed structure for seed.yaml
|
||||
type seed struct {
|
||||
Snaps []struct {
|
||||
Name string `yaml:"name"`
|
||||
Channel string `yaml:"channel"`
|
||||
File string `yaml:"file"`
|
||||
} `yaml:"snaps"`
|
||||
}
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
storeClient *store.Store
|
||||
verbose bool
|
||||
currentSnaps []*store.CurrentSnap
|
||||
requiredSnaps map[string]bool
|
||||
processedSnaps = make(map[string]bool)
|
||||
snapSizeMap = make(map[string]float64)
|
||||
totalSnapSize float64
|
||||
)
|
||||
|
||||
type SnapInfo struct {
|
||||
InstanceName string
|
||||
SnapID string
|
||||
Revision snap.Revision
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Override the default plug slot sanitizer
|
||||
snap.SanitizePlugsSlots = sanitizePlugsSlots
|
||||
|
||||
// Initialize progress reporting
|
||||
InitProgress()
|
||||
totalSnapSize = 0
|
||||
|
||||
// Initialize the store client
|
||||
storeClient = store.New(nil, nil)
|
||||
|
||||
// Parse command-line flags
|
||||
var seedDirectory string
|
||||
flag.StringVar(&seedDirectory, "seed", "/var/lib/snapd/seed", "Specify the seed directory")
|
||||
flag.BoolVar(&verbose, "verbose", false, "Enable verbose output")
|
||||
flag.Parse()
|
||||
if !verbose {
|
||||
fmt.Printf("2\tLoading existing snaps...\n")
|
||||
}
|
||||
|
||||
// Define directories based on the seed directory
|
||||
snapsDir := filepath.Join(seedDirectory, "snaps")
|
||||
assertionsDir := filepath.Join(seedDirectory, "assertions")
|
||||
seedYaml := filepath.Join(seedDirectory, "seed.yaml")
|
||||
|
||||
// Setup directories and seed.yaml
|
||||
initializeDirectories(snapsDir, assertionsDir)
|
||||
initializeSeedYaml(seedYaml)
|
||||
|
||||
// Load existing snaps from seed.yaml
|
||||
existingSnapsInYaml := loadExistingSnaps(seedYaml)
|
||||
|
||||
// Populate currentSnaps based on existing snaps
|
||||
for snapName := range existingSnapsInYaml {
|
||||
snapInfo, err := getCurrentSnapInfo(assertionsDir, snapName)
|
||||
if err != nil {
|
||||
verboseLog("Failed to get info for existing snap %s: %v", snapName, err)
|
||||
continue
|
||||
}
|
||||
currentSnaps = append(currentSnaps, snapInfo)
|
||||
}
|
||||
|
||||
// Process essential snaps
|
||||
requiredSnaps = map[string]bool{"snapd": true, "bare": true}
|
||||
for _, arg := range flag.Args() {
|
||||
requiredSnaps[arg] = true
|
||||
}
|
||||
if !verbose {
|
||||
fmt.Printf("4\tFetching information from the Snap Store...\n")
|
||||
}
|
||||
|
||||
// Collect snaps to process
|
||||
snapsToProcess, err := collectSnapsToProcess(snapsDir, assertionsDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to collect snaps to process: %v", err)
|
||||
}
|
||||
|
||||
progressTracker.Finish("Finished collecting snap info")
|
||||
|
||||
// Calculate the number of snaps to download
|
||||
totalSnaps := len(snapsToProcess)
|
||||
if totalSnaps == 0 {
|
||||
verboseLog("No snaps to process.")
|
||||
} else {
|
||||
verboseLog("Total snaps to download: %d", totalSnaps)
|
||||
}
|
||||
|
||||
// Initialize variables to track download progress
|
||||
completedSnaps := 0
|
||||
|
||||
// Update "Downloading snaps" step to 0%
|
||||
progressTracker.UpdateStepProgress(0)
|
||||
|
||||
// Process all the snaps that need updates
|
||||
for _, snapDetails := range snapsToProcess {
|
||||
if err := processSnap(snapDetails, snapsDir, assertionsDir); err != nil {
|
||||
log.Fatalf("Failed to process snap %s: %v", snapDetails.InstanceName, err)
|
||||
}
|
||||
completedSnaps++
|
||||
|
||||
progressTracker.UpdateStepProgress(-1)
|
||||
}
|
||||
|
||||
// Mark "Downloading snaps" as complete
|
||||
if totalSnaps > 0 {
|
||||
progressTracker.Finish("Downloading snaps completed")
|
||||
} else {
|
||||
// If no snaps to download, skip to finalizing
|
||||
progressTracker.NextStep()
|
||||
}
|
||||
|
||||
// Remove unnecessary snaps after processing dependencies
|
||||
cleanUpCurrentSnaps(assertionsDir, snapsDir)
|
||||
|
||||
// Update seed.yaml with the current required snaps
|
||||
if err := updateSeedYaml(snapsDir, seedYaml, currentSnaps); err != nil {
|
||||
log.Fatalf("Failed to update seed.yaml: %v", err)
|
||||
}
|
||||
|
||||
// Perform cleanup and validation tasks
|
||||
removeStateJson(filepath.Join(seedDirectory, "..", "state.json"))
|
||||
ensureAssertions(assertionsDir)
|
||||
if err := validateSeed(seedYaml); err != nil {
|
||||
log.Fatalf("Seed validation failed: %v", err)
|
||||
}
|
||||
cleanUpFiles(snapsDir, assertionsDir, seedYaml)
|
||||
|
||||
// Mark "Finalizing" as complete
|
||||
if progressTracker != nil {
|
||||
progressTracker.Finish("Cleanup and validation completed")
|
||||
}
|
||||
}
|
||||
|
||||
// collectSnapsToProcess collects all snaps and their dependencies, returning only those that need updates
|
||||
func collectSnapsToProcess(snapsDir, assertionsDir string) ([]SnapDetails, error) {
|
||||
var snapsToProcess []SnapDetails
|
||||
|
||||
for snapEntry := range requiredSnaps {
|
||||
// Extract channel if specified, default to "stable"
|
||||
parts := strings.SplitN(snapEntry, "=", 2)
|
||||
channel := "stable"
|
||||
if len(parts) == 2 {
|
||||
channel = parts[1]
|
||||
}
|
||||
snapName := parts[0]
|
||||
|
||||
// Collect snap dependencies and their statuses
|
||||
snapList, err := collectSnapDependencies(snapName, channel, snapsDir, assertionsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Append only those snaps that need updates
|
||||
for _, snapDetails := range snapList {
|
||||
verboseLog("Processing snap: %s with result: %v", snapDetails.InstanceName, snapDetails.Result)
|
||||
if len(snapDetails.Result.Deltas) > 0 {
|
||||
for _, delta := range snapDetails.Result.Deltas {
|
||||
snapSize := float64(delta.Size)
|
||||
snapSizeMap[snapDetails.Result.Info.SuggestedName] = snapSize
|
||||
totalSnapSize += snapSize
|
||||
}
|
||||
} else {
|
||||
snapSize := float64(snapDetails.Result.Info.Size)
|
||||
snapSizeMap[snapDetails.Result.Info.SuggestedName] = snapSize
|
||||
totalSnapSize += snapSize
|
||||
}
|
||||
snapsToProcess = append(snapsToProcess, snapDetails)
|
||||
}
|
||||
}
|
||||
|
||||
return snapsToProcess, nil
|
||||
}
|
||||
|
||||
// sanitizePlugsSlots is a placeholder function to sanitize plug slots in snap.Info
|
||||
func sanitizePlugsSlots(info *snap.Info) {}
|
||||
|
||||
// verboseLog logs messages only when verbose mode is enabled
|
||||
func verboseLog(format string, v ...interface{}) {
|
||||
if verbose {
|
||||
log.Printf(format, v...)
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/snapcore/snapd/snap"
|
||||
"github.com/snapcore/snapd/store"
|
||||
)
|
||||
|
||||
// SnapDetails struct remains unchanged
|
||||
type SnapDetails struct {
|
||||
InstanceName string
|
||||
Channel string
|
||||
CurrentSnap *store.CurrentSnap
|
||||
Result *store.SnapActionResult
|
||||
}
|
||||
|
||||
// collectSnapDependencies collects all dependencies for a given snap, marking them as requiredSnaps regardless of whether they need updates.
|
||||
func collectSnapDependencies(snapName, channel, snapsDir, assertionsDir string) ([]SnapDetails, error) {
|
||||
var snapDetailsList []SnapDetails
|
||||
|
||||
if processedSnaps[snapName] {
|
||||
verboseLog("Snap %s has already been processed. Skipping.", snapName)
|
||||
return snapDetailsList, nil // Added 0 for int64
|
||||
}
|
||||
|
||||
oldSnapPath, oldSnap := findPreviousSnap(snapsDir, assertionsDir, snapName)
|
||||
|
||||
var result *store.SnapActionResult
|
||||
var err error
|
||||
|
||||
// Fetch or refresh snap information
|
||||
if oldSnap == nil || oldSnap.SnapID == "" || oldSnap.Revision.N == 0 {
|
||||
result, err = fetchOrRefreshSnapInfo(snapName, nil, channel)
|
||||
if err != nil {
|
||||
return nil, err // Added 0 for int64
|
||||
}
|
||||
} else {
|
||||
result, err = fetchOrRefreshSnapInfo(snapName, oldSnap, channel)
|
||||
if err != nil && strings.Contains(err.Error(), "snap has no updates available") {
|
||||
result, err = fetchOrRefreshSnapInfo(snapName, nil, channel)
|
||||
if err != nil {
|
||||
return nil, err // Added 0 for int64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result == nil || result.Info == nil || result.Info.SnapID == "" || result.Info.Revision.N == 0 {
|
||||
return nil, fmt.Errorf("invalid snap information returned for %s: SnapID or Revision is missing", snapName) // Added 0 for int64
|
||||
}
|
||||
|
||||
info := result.Info
|
||||
newSnap := &store.CurrentSnap{
|
||||
InstanceName: snapName,
|
||||
SnapID: info.SnapID,
|
||||
Revision: snap.Revision{N: info.Revision.N},
|
||||
}
|
||||
snapInCurrentSnaps, oldRevision := isSnapInCurrentSnaps(snapName)
|
||||
if snapInCurrentSnaps {
|
||||
removeSnapFromCurrentSnaps(snapName, oldRevision)
|
||||
}
|
||||
currentSnaps = append(currentSnaps, newSnap)
|
||||
processedSnaps[snapName] = true
|
||||
|
||||
needsUpdate := (oldSnapPath == "" || oldSnap.Revision.N < info.Revision.N)
|
||||
|
||||
if needsUpdate {
|
||||
snapDetailsList = append(snapDetailsList, SnapDetails{
|
||||
InstanceName: snapName,
|
||||
Channel: channel,
|
||||
CurrentSnap: newSnap,
|
||||
Result: result,
|
||||
})
|
||||
} else {
|
||||
// Mark the snap as required even if no update is needed
|
||||
requiredSnaps[snapName] = true
|
||||
}
|
||||
|
||||
// Safely handle dependencies
|
||||
tracker := snap.SimplePrereqTracker{}
|
||||
missingPrereqs := tracker.MissingProviderContentTags(info, nil)
|
||||
for prereq := range missingPrereqs {
|
||||
if !processedSnaps[prereq] {
|
||||
verboseLog("Collecting dependencies for prerequisite snap: %s for %s", prereq, snapName)
|
||||
prereqDetails, err := collectSnapDependencies(prereq, "stable", snapsDir, assertionsDir)
|
||||
if err != nil {
|
||||
// Additional logging for dependency resolution issues
|
||||
verboseLog("Failed to collect dependencies for prerequisite %s for snap %s: %v", prereq, snapName, err)
|
||||
return nil, fmt.Errorf("failed to collect dependencies for prerequisite %s for snap %s: %v", prereq, snapName, err) // Added 0 for int64
|
||||
}
|
||||
snapDetailsList = append(snapDetailsList, prereqDetails...)
|
||||
}
|
||||
}
|
||||
|
||||
// Also handle base snaps safely
|
||||
if info.Base != "" && !processedSnaps[info.Base] {
|
||||
verboseLog("Collecting dependencies for base snap: %s for %s", info.Base, snapName)
|
||||
baseDetails, err := collectSnapDependencies(info.Base, "stable", snapsDir, assertionsDir)
|
||||
if err != nil {
|
||||
verboseLog("Failed to collect dependencies for base snap %s for snap %s: %v", info.Base, snapName, err)
|
||||
return nil, fmt.Errorf("failed to collect dependencies for base snap %s for snap %s: %v", info.Base, snapName, err) // Added 0 for int64
|
||||
}
|
||||
snapDetailsList = append(snapDetailsList, baseDetails...)
|
||||
}
|
||||
|
||||
return snapDetailsList, nil
|
||||
}
|
||||
|
||||
// processSnap handles the downloading and applying of a snap if updates are available.
|
||||
// It gracefully handles the "no updates available" scenario and ensures dependencies are marked as required.
|
||||
func processSnap(snapDetails SnapDetails, snapsDir, assertionsDir string) error {
|
||||
verboseLog("Processing snap: %s on channel: %s", snapDetails.InstanceName, snapDetails.Channel)
|
||||
|
||||
// Proceed with downloading the snap (either full or delta) using downloadAndApplySnap
|
||||
snapInfo, err := downloadAndApplySnap(storeClient, snapDetails.Result, snapsDir, assertionsDir, snapDetails.CurrentSnap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download snap %s: %w", snapDetails.InstanceName, err)
|
||||
}
|
||||
|
||||
// Mark the snap as required after successful download and application
|
||||
requiredSnaps[snapDetails.InstanceName] = true
|
||||
verboseLog("Downloaded and applied snap: %s, revision: %d", snapInfo.SuggestedName, snapInfo.Revision.N)
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchOrRefreshSnapInfo retrieves snap information and returns the SnapActionResult including deltas.
|
||||
func fetchOrRefreshSnapInfo(snapName string, currentSnap *store.CurrentSnap, channel string) (*store.SnapActionResult, error) {
|
||||
var actions []*store.SnapAction
|
||||
var includeSnap []*store.CurrentSnap
|
||||
if currentSnap != nil {
|
||||
verboseLog("Crafting refresh SnapAction for %s", snapName)
|
||||
actions = append(actions, &store.SnapAction{
|
||||
Action: "refresh",
|
||||
SnapID: currentSnap.SnapID,
|
||||
InstanceName: snapName,
|
||||
Channel: channel,
|
||||
})
|
||||
includeSnap = []*store.CurrentSnap{currentSnap}
|
||||
} else {
|
||||
verboseLog("Crafting install SnapAction for %s", snapName)
|
||||
actions = append(actions, &store.SnapAction{
|
||||
Action: "install",
|
||||
InstanceName: snapName,
|
||||
Channel: channel,
|
||||
})
|
||||
includeSnap = []*store.CurrentSnap{}
|
||||
}
|
||||
|
||||
results, _, err := storeClient.SnapAction(ctx, includeSnap, actions, nil, nil, nil)
|
||||
if err != nil {
|
||||
verboseLog("SnapAction error for %s: %v", snapName, err)
|
||||
if strings.Contains(err.Error(), "snap has no updates available") && currentSnap != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("snap action failed for %s: %w", snapName, err)
|
||||
}
|
||||
|
||||
if len(results) == 0 || results[0].Info == nil {
|
||||
return nil, fmt.Errorf("no snap info returned for snap %s", snapName)
|
||||
}
|
||||
|
||||
result := &results[0]
|
||||
info := result.Info
|
||||
|
||||
// Validate necessary fields in the snap information
|
||||
if info.SnapID == "" || info.Revision.N == 0 {
|
||||
return nil, fmt.Errorf("invalid snap information for %s: SnapID or Revision is missing", snapName)
|
||||
}
|
||||
|
||||
verboseLog("Fetched latest snap info for %s: SnapID: %s, Revision: %d", snapName, info.SnapID, info.Revision.N)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// findPreviousSnap locates the previous snap revision in the downloads directory.
|
||||
func findPreviousSnap(downloadDir, assertionsDir, snapName string) (string, *store.CurrentSnap) {
|
||||
var currentSnap store.CurrentSnap
|
||||
files, err := os.ReadDir(downloadDir)
|
||||
if err != nil {
|
||||
verboseLog("Error reading directory: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var latestRevision int
|
||||
var latestSnapPath string
|
||||
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), snapName+"_") && strings.HasSuffix(file.Name(), ".snap") {
|
||||
revisionStr := extractRevisionFromFile(file.Name())
|
||||
if revisionStr == "" {
|
||||
verboseLog("Failed to extract revision from file name: %s", file.Name())
|
||||
continue
|
||||
}
|
||||
revision, err := strconv.Atoi(revisionStr)
|
||||
if err != nil {
|
||||
verboseLog("Failed to parse revision number for file %s: %v", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
verboseLog("Found %s with revision %d", file.Name(), revision)
|
||||
|
||||
if revision > latestRevision {
|
||||
latestRevision = revision
|
||||
latestSnapPath = filepath.Join(downloadDir, file.Name())
|
||||
|
||||
// Parse the corresponding assertion file
|
||||
assertFilePath := filepath.Join(assertionsDir, strings.Replace(file.Name(), ".snap", ".assert", 1))
|
||||
currentSnap = parseSnapInfo(assertFilePath, snapName)
|
||||
currentSnap.Revision.N = revision
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if latestSnapPath != "" {
|
||||
return latestSnapPath, ¤tSnap
|
||||
}
|
||||
return "", nil
|
||||
}
|
@ -0,0 +1,286 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"github.com/snapcore/snapd/progress"
|
||||
)
|
||||
|
||||
var (
|
||||
progressReporter ProgressReporter
|
||||
progressTracker *ProgressTracker
|
||||
globalDownloaded float64
|
||||
globalMu sync.Mutex
|
||||
lastReported int
|
||||
)
|
||||
|
||||
type ProgressReporter interface {
|
||||
Report(percentage int, status string)
|
||||
}
|
||||
|
||||
// VerboseProgressReporter formats progress updates
|
||||
type VerboseProgressReporter struct{}
|
||||
|
||||
func (v *VerboseProgressReporter) Report(percentage int, status string) {
|
||||
fmt.Printf("%d\t%s\n", percentage, status)
|
||||
}
|
||||
|
||||
// ProgressMeter tracks the download progress and implements the progress.Meter interface
|
||||
type ProgressMeter struct {
|
||||
currentBytes float64
|
||||
isDelta bool
|
||||
mu sync.Mutex
|
||||
snapName string
|
||||
snapVersion string
|
||||
totalSize float64
|
||||
}
|
||||
|
||||
// Ensure ProgressMeter implements the progress.Meter interface
|
||||
var _ progress.Meter = (*ProgressMeter)(nil)
|
||||
|
||||
// NewProgressMeter initializes a new ProgressMeter instance with the snap name
|
||||
func NewProgressMeter(snapName string, snapVersion string, isDelta bool) *ProgressMeter {
|
||||
return &ProgressMeter{
|
||||
isDelta: isDelta,
|
||||
snapName: snapName,
|
||||
snapVersion: snapVersion,
|
||||
totalSize: snapSizeMap[snapName],
|
||||
}
|
||||
}
|
||||
|
||||
// Start initializes the progress meter with the total size
|
||||
func (pm *ProgressMeter) Start(label string, total float64) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
pm.totalSize = total
|
||||
|
||||
// Update global total size
|
||||
globalMu.Lock()
|
||||
globalMu.Unlock()
|
||||
}
|
||||
|
||||
// Set updates the progress based on the current value representing the number of bytes downloaded
|
||||
func (pm *ProgressMeter) Set(value float64) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
delta := value - pm.currentBytes
|
||||
pm.currentBytes = value
|
||||
|
||||
// Update global downloaded bytes
|
||||
globalMu.Lock()
|
||||
globalDownloaded += delta
|
||||
globalMu.Unlock()
|
||||
|
||||
reportGlobalProgress(pm.snapName, pm.snapVersion, pm.isDelta)
|
||||
}
|
||||
|
||||
// SetTotal sets the total size for the ProgressMeter
|
||||
func (pm *ProgressMeter) SetTotal(total float64) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
pm.totalSize = total
|
||||
}
|
||||
|
||||
// Finished marks the progress as complete
|
||||
func (pm *ProgressMeter) Finished() {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
delta := pm.totalSize - pm.currentBytes
|
||||
pm.currentBytes = pm.totalSize
|
||||
|
||||
// Update global downloaded bytes
|
||||
globalMu.Lock()
|
||||
globalDownloaded += delta
|
||||
globalMu.Unlock()
|
||||
|
||||
reportGlobalProgress(pm.snapName, pm.snapVersion, pm.isDelta)
|
||||
}
|
||||
|
||||
// Write handles byte data to update progress based on the size of the data written
|
||||
func (pm *ProgressMeter) Write(p []byte) (n int, err error) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
// Calculate the delta and update the current bytes
|
||||
delta := float64(len(p))
|
||||
pm.currentBytes += delta
|
||||
|
||||
// Update global downloaded bytes
|
||||
globalMu.Lock()
|
||||
globalDownloaded += delta
|
||||
globalMu.Unlock()
|
||||
|
||||
reportGlobalProgress(pm.snapName, pm.snapVersion, pm.isDelta)
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// reportGlobalProgress calculates and formats the overall progress percentage
|
||||
func reportGlobalProgress(snapName string, snapVersion string, isDelta bool) {
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
if totalSnapSize == 0 {
|
||||
return
|
||||
}
|
||||
// Calculate the percentage within the range of 10 to 90
|
||||
percentage := int((globalDownloaded / totalSnapSize) * 80) + 10
|
||||
|
||||
// Only print if there's a change in percentage to reduce output
|
||||
if percentage != lastReported {
|
||||
lastReported = percentage
|
||||
if isDelta {
|
||||
progressString := fmt.Sprintf("Downloading delta for snap %s %s", snapName, snapVersion)
|
||||
progressReporter.Report(percentage, progressString)
|
||||
} else {
|
||||
progressString := fmt.Sprintf("Downloading snap %s %s", snapName, snapVersion)
|
||||
progressReporter.Report(percentage, progressString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spin shows indefinite activity; not used in this implementation
|
||||
func (pm *ProgressMeter) Spin(msg string) {
|
||||
fmt.Printf("Spin: %s\n", msg)
|
||||
}
|
||||
|
||||
// Notify formats notifications about the progress
|
||||
func (pm *ProgressMeter) Notify(message string) {
|
||||
fmt.Printf("Notification: %s\n", message)
|
||||
}
|
||||
|
||||
// ProgressTracker manages multiple steps of progress
|
||||
type ProgressTracker struct {
|
||||
totalWeight int
|
||||
completedWeight float64
|
||||
mu sync.Mutex
|
||||
reporter ProgressReporter
|
||||
steps []*WeightedStep
|
||||
currentStep int
|
||||
}
|
||||
|
||||
// WeightedStep represents a step in a multi-step progress tracker
|
||||
type WeightedStep struct {
|
||||
Weight int
|
||||
Progress float64
|
||||
Status string
|
||||
}
|
||||
|
||||
// NewProgressTracker creates a new instance of ProgressTracker
|
||||
func NewProgressTracker(reporter ProgressReporter) *ProgressTracker {
|
||||
return &ProgressTracker{
|
||||
reporter: reporter,
|
||||
steps: []*WeightedStep{},
|
||||
}
|
||||
}
|
||||
|
||||
// AddStep adds a new step to the progress tracker
|
||||
func (pt *ProgressTracker) AddStep(weight int, status string) {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
pt.steps = append(pt.steps, &WeightedStep{
|
||||
Weight: weight,
|
||||
Status: status,
|
||||
})
|
||||
pt.totalWeight += weight
|
||||
}
|
||||
|
||||
// Start initializes the first step of the tracker
|
||||
func (pt *ProgressTracker) Start() {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
if len(pt.steps) > 0 {
|
||||
pt.currentStep = 0
|
||||
pt.steps[pt.currentStep].Progress = 0
|
||||
pt.reportProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateStepProgress updates the current step's progress
|
||||
func (pt *ProgressTracker) UpdateStepProgress(progress float64) {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
if pt.currentStep >= len(pt.steps) {
|
||||
return
|
||||
}
|
||||
step := pt.steps[pt.currentStep]
|
||||
if progress < 0 {
|
||||
progress = float64(int((globalDownloaded / totalSnapSize) * float64(step.Weight)))
|
||||
} else if progress < step.Progress {
|
||||
return
|
||||
}
|
||||
// Adjust delta calculation
|
||||
delta := (progress - step.Progress)
|
||||
pt.completedWeight += delta
|
||||
step.Progress = progress
|
||||
//pt.reportProgress()
|
||||
}
|
||||
|
||||
func (pt *ProgressTracker) Finish(status string) {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
if len(pt.steps) == 0 || pt.currentStep >= len(pt.steps) {
|
||||
return
|
||||
}
|
||||
step := pt.steps[pt.currentStep]
|
||||
if step.Progress < 100.0 {
|
||||
// Corrected calculation to prevent exceeding 100%
|
||||
remainingProgress := 100.0 - step.Progress
|
||||
delta := (remainingProgress * float64(step.Weight)) / 100.0
|
||||
pt.completedWeight += delta
|
||||
step.Progress = 100.0
|
||||
}
|
||||
percentage := pt.calculatePercentage()
|
||||
pt.reporter.Report(percentage, status)
|
||||
if pt.currentStep < len(pt.steps)-1 {
|
||||
pt.currentStep++
|
||||
pt.steps[pt.currentStep].Progress = 0
|
||||
pt.reportProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// NextStep moves to the next step in the progress tracker
|
||||
func (pt *ProgressTracker) NextStep() {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
if pt.currentStep < len(pt.steps)-1 {
|
||||
pt.currentStep++
|
||||
pt.steps[pt.currentStep].Progress = 0
|
||||
pt.reportProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// calculatePercentage calculates the overall progress as a percentage
|
||||
func (pt *ProgressTracker) calculatePercentage() int {
|
||||
if pt.totalWeight == 0 {
|
||||
return 0
|
||||
}
|
||||
// Calculate percentage accurately and scale within 0-99 range
|
||||
percentage := int((pt.completedWeight / float64(pt.totalWeight)) * 100)
|
||||
if percentage > 99 {
|
||||
percentage = 99
|
||||
}
|
||||
return percentage
|
||||
}
|
||||
|
||||
// reportProgress reports the current progress percentage to the reporter
|
||||
func (pt *ProgressTracker) reportProgress() {
|
||||
percentage := pt.calculatePercentage()
|
||||
status := pt.steps[pt.currentStep].Status
|
||||
if percentage != lastReported {
|
||||
pt.reporter.Report(percentage, status)
|
||||
lastReported = percentage
|
||||
}
|
||||
}
|
||||
|
||||
// InitProgress initializes the global progress tracker and sets up steps
|
||||
func InitProgress() {
|
||||
progressReporter = &VerboseProgressReporter{}
|
||||
progressTracker = NewProgressTracker(progressReporter)
|
||||
progressTracker.AddStep(10, "Initialization")
|
||||
progressTracker.AddStep(80, "Downloading snaps")
|
||||
progressTracker.AddStep(10, "Verifying snaps")
|
||||
progressTracker.Start()
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/snapcore/snapd/store"
|
||||
)
|
||||
|
||||
// initializeSeedYaml ensures that seed.yaml exists; if not, creates it.
|
||||
func initializeSeedYaml(seedYaml string) {
|
||||
if _, err := os.Stat(seedYaml); os.IsNotExist(err) {
|
||||
file, err := os.Create(seedYaml)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create seed.yaml: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
file.WriteString("snaps:\n")
|
||||
}
|
||||
}
|
||||
|
||||
// loadSeedData loads seed data from seed.yaml
|
||||
func loadSeedData(seedYaml string) seed {
|
||||
file, err := ioutil.ReadFile(seedYaml)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read seed.yaml: %v", err)
|
||||
}
|
||||
|
||||
var seedData seed
|
||||
if err := yaml.Unmarshal(file, &seedData); err != nil {
|
||||
log.Fatalf("Failed to parse seed.yaml: %v", err)
|
||||
}
|
||||
|
||||
return seedData
|
||||
}
|
||||
|
||||
// loadExistingSnaps loads snaps from seed.yaml into a map
|
||||
func loadExistingSnaps(seedYaml string) map[string]bool {
|
||||
file, err := ioutil.ReadFile(seedYaml)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read seed.yaml: %v", err)
|
||||
}
|
||||
|
||||
var seedData seed
|
||||
if err := yaml.Unmarshal(file, &seedData); err != nil {
|
||||
log.Fatalf("Failed to parse seed.yaml: %v", err)
|
||||
}
|
||||
|
||||
existing := make(map[string]bool)
|
||||
for _, snap := range seedData.Snaps {
|
||||
existing[snap.Name] = true
|
||||
verboseLog("Found %s in seed.yaml\n", snap.Name)
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
// updateSeedYaml updates the seed.yaml file with the current required snaps
|
||||
func updateSeedYaml(snapsDir, seedYaml string, currentSnaps []*store.CurrentSnap) error {
|
||||
// Log the snaps to be written
|
||||
verboseLog("CurrentSnaps to be written to seed.yaml:")
|
||||
for _, snapInfo := range currentSnaps {
|
||||
verboseLog("- %s_%d.snap", snapInfo.InstanceName, snapInfo.Revision.N)
|
||||
}
|
||||
|
||||
// Load existing seed data
|
||||
file, err := ioutil.ReadFile(seedYaml)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read seed.yaml: %w", err)
|
||||
}
|
||||
|
||||
var seedData seed
|
||||
if err := yaml.Unmarshal(file, &seedData); err != nil {
|
||||
return fmt.Errorf("failed to parse seed.yaml: %w", err)
|
||||
}
|
||||
|
||||
// Clear existing snaps
|
||||
seedData.Snaps = []struct {
|
||||
Name string `yaml:"name"`
|
||||
Channel string `yaml:"channel"`
|
||||
File string `yaml:"file"`
|
||||
}{}
|
||||
|
||||
// Populate seedData with currentSnaps
|
||||
for _, snapInfo := range currentSnaps {
|
||||
snapFileName := fmt.Sprintf("%s_%d.snap", snapInfo.InstanceName, snapInfo.Revision.N)
|
||||
snapData := struct {
|
||||
Name string `yaml:"name"`
|
||||
Channel string `yaml:"channel"`
|
||||
File string `yaml:"file"`
|
||||
}{
|
||||
Name: snapInfo.InstanceName,
|
||||
Channel: "stable", // Assuming 'stable' channel; modify as needed
|
||||
File: snapFileName,
|
||||
}
|
||||
seedData.Snaps = append(seedData.Snaps, snapData)
|
||||
}
|
||||
|
||||
// Marshal the updated seedData back to YAML
|
||||
updatedYAML, err := yaml.Marshal(&seedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal updated seed data: %w", err)
|
||||
}
|
||||
|
||||
// Write the updated YAML back to seed.yaml
|
||||
if err := ioutil.WriteFile(seedYaml, updatedYAML, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write updated seed.yaml: %w", err)
|
||||
}
|
||||
|
||||
verboseLog("Updated seed.yaml with current snaps.")
|
||||
return nil
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/snapcore/snapd/snap"
|
||||
"github.com/snapcore/snapd/store"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// verifyChecksum calculates the SHA3-384 checksum of a file and compares it with the expected checksum.
|
||||
func verifyChecksum(filePath, expectedChecksum string) (bool, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to open file for checksum verification: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := sha3.New384()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return false, fmt.Errorf("failed to calculate checksum: %w", err)
|
||||
}
|
||||
|
||||
calculatedChecksum := hex.EncodeToString(hash.Sum(nil))
|
||||
return strings.EqualFold(calculatedChecksum, expectedChecksum), nil
|
||||
}
|
||||
|
||||
// extractRevisionFromFile extracts the revision number from a file name by splitting at the last underscore.
|
||||
func extractRevisionFromFile(fileName string) string {
|
||||
lastUnderscore := strings.LastIndex(fileName, "_")
|
||||
if lastUnderscore == -1 {
|
||||
return ""
|
||||
}
|
||||
revisionWithSuffix := fileName[lastUnderscore+1:]
|
||||
// Handle both .snap and .assert suffixes
|
||||
if strings.HasSuffix(revisionWithSuffix, ".snap") {
|
||||
return strings.TrimSuffix(revisionWithSuffix, ".snap")
|
||||
} else if strings.HasSuffix(revisionWithSuffix, ".assert") {
|
||||
return strings.TrimSuffix(revisionWithSuffix, ".assert")
|
||||
}
|
||||
return revisionWithSuffix
|
||||
}
|
||||
|
||||
// fileExists checks if a file exists and is not a directory before we use it
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
// parseSnapInfo extracts snap details from the assertion file and returns the snap's current revision.
|
||||
func parseSnapInfo(assertFilePath, snapName string) store.CurrentSnap {
|
||||
currentSnap := store.CurrentSnap{InstanceName: snapName}
|
||||
file, err := os.Open(assertFilePath)
|
||||
if err != nil {
|
||||
verboseLog("Failed to read assertion file: %v\n", err)
|
||||
return currentSnap
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(line, "snap-id:") {
|
||||
currentSnap.SnapID = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
|
||||
}
|
||||
if strings.HasPrefix(line, "snap-revision:") {
|
||||
revisionStr := strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
|
||||
revision, err := strconv.Atoi(revisionStr)
|
||||
if err != nil {
|
||||
verboseLog("Failed to parse snap-revision for snap %s: %v", snapName, err)
|
||||
continue
|
||||
}
|
||||
currentSnap.Revision.N = revision
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that required fields are present
|
||||
if currentSnap.SnapID == "" || currentSnap.Revision.N == 0 {
|
||||
verboseLog("Incomplete snap info in assertion file: %s", assertFilePath)
|
||||
}
|
||||
|
||||
return currentSnap
|
||||
}
|
||||
|
||||
// isSnapInCurrentSnaps checks if a snap is already in currentSnaps
|
||||
func isSnapInCurrentSnaps(snapName string) (bool, snap.Revision) {
|
||||
for _, snap := range currentSnaps {
|
||||
if snap.InstanceName == snapName {
|
||||
return true, snap.Revision
|
||||
}
|
||||
}
|
||||
return false, snap.Revision{}
|
||||
}
|
||||
|
||||
func removeSnapFromCurrentSnaps(snapName string, revision snap.Revision) {
|
||||
for i := 0; i < len(currentSnaps); i++ {
|
||||
if currentSnaps[i].Revision == revision {
|
||||
// Remove the element at index i
|
||||
currentSnaps = append(currentSnaps[:i], currentSnaps[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initializeDirectories ensures that the snaps and assertions directories exist
|
||||
func initializeDirectories(snapsDir, assertionsDir string) {
|
||||
if err := os.MkdirAll(snapsDir, 0755); err != nil {
|
||||
log.Fatalf("Failed to create snaps directory: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(assertionsDir, 0755); err != nil {
|
||||
log.Fatalf("Failed to create assertions directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// getCurrentSnapInfo retrieves current snap information from assertions
|
||||
func getCurrentSnapInfo(assertionsDir, snapName string) (*store.CurrentSnap, error) {
|
||||
assertionFiles, err := filepath.Glob(filepath.Join(assertionsDir, fmt.Sprintf("%s_*.assert", snapName)))
|
||||
if err != nil || len(assertionFiles) == 0 {
|
||||
return nil, fmt.Errorf("no assertion file found for snap: %s", snapName)
|
||||
}
|
||||
|
||||
assertionFile := assertionFiles[0]
|
||||
currentSnap := parseSnapInfo(assertionFile, snapName)
|
||||
if currentSnap.SnapID == "" || currentSnap.Revision.N == 0 {
|
||||
return nil, fmt.Errorf("incomplete snap info in assertion file for snap: %s", snapName)
|
||||
}
|
||||
verboseLog("Found snap info for %s: SnapID: %s, Revision: %d", snapName, currentSnap.SnapID, currentSnap.Revision.N)
|
||||
return ¤tSnap, nil
|
||||
}
|
||||
|
||||
// verifySnapIntegrity verifies the integrity of a snap file
|
||||
func verifySnapIntegrity(filePath, expectedChecksum string) bool {
|
||||
checksumMatches, err := verifyChecksum(filePath, expectedChecksum)
|
||||
if err != nil {
|
||||
verboseLog("Checksum verification failed for %s: %v", filePath, err)
|
||||
return false
|
||||
}
|
||||
return checksumMatches
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// validateSeed validates the seed using snap debug
|
||||
func validateSeed(seedYaml string) error {
|
||||
cmd := exec.Command("snap", "debug", "validate-seed", seedYaml)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validation failed with output: %s, error: %v", string(output), err)
|
||||
}
|
||||
verboseLog("Seed validation successful: %s", string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureAssertions ensures that essential assertions are present
|
||||
func ensureAssertions(assertionsDir string) {
|
||||
model := "generic-classic"
|
||||
brand := "generic"
|
||||
series := "16" // Hardcoded series as snap.Info does not have a Series field
|
||||
|
||||
modelAssertionPath := filepath.Join(assertionsDir, "model")
|
||||
accountKeyAssertionPath := filepath.Join(assertionsDir, "account-key")
|
||||
accountAssertionPath := filepath.Join(assertionsDir, "account")
|
||||
|
||||
// Check and generate model assertion
|
||||
if _, err := os.Stat(modelAssertionPath); os.IsNotExist(err) {
|
||||
output, err := exec.Command("snap", "known", "--remote", "model", "series="+series, "model="+model, "brand-id="+brand).CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch model assertion: %v, Output: %s", err, string(output))
|
||||
}
|
||||
if err := ioutil.WriteFile(modelAssertionPath, output, 0644); err != nil {
|
||||
log.Fatalf("Failed to write model assertion: %v", err)
|
||||
}
|
||||
verboseLog("Fetched and saved model assertion to %s", modelAssertionPath)
|
||||
}
|
||||
|
||||
// Generate account-key assertion if not exists
|
||||
if _, err := os.Stat(accountKeyAssertionPath); os.IsNotExist(err) {
|
||||
signKeySha3 := grepPattern(modelAssertionPath, "sign-key-sha3-384: ")
|
||||
if signKeySha3 == "" {
|
||||
log.Fatalf("Failed to extract sign-key-sha3-384 from model assertion.")
|
||||
}
|
||||
decodedSignKey, err := base64.StdEncoding.DecodeString(signKeySha3)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to decode sign-key-sha3-384: %v", err)
|
||||
}
|
||||
encodedSignKey := base64.StdEncoding.EncodeToString(decodedSignKey)
|
||||
output, err := exec.Command("snap", "known", "--remote", "account-key", "public-key-sha3-384="+encodedSignKey).CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch account-key assertion: %v, Output: %s", err, string(output))
|
||||
}
|
||||
if err := ioutil.WriteFile(accountKeyAssertionPath, output, 0644); err != nil {
|
||||
log.Fatalf("Failed to write account-key assertion: %v", err)
|
||||
}
|
||||
verboseLog("Fetched and saved account-key assertion to %s", accountKeyAssertionPath)
|
||||
}
|
||||
|
||||
// Generate account assertion if not exists
|
||||
if _, err := os.Stat(accountAssertionPath); os.IsNotExist(err) {
|
||||
accountId := grepPattern(accountKeyAssertionPath, "account-id: ")
|
||||
if accountId == "" {
|
||||
log.Fatalf("Failed to extract account-id from account-key assertion.")
|
||||
}
|
||||
output, err := exec.Command("snap", "known", "--remote", "account", "account-id="+accountId).CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch account assertion: %v, Output: %s", err, string(output))
|
||||
}
|
||||
if err := ioutil.WriteFile(accountAssertionPath, output, 0644); err != nil {
|
||||
log.Fatalf("Failed to write account assertion: %v", err)
|
||||
}
|
||||
verboseLog("Fetched and saved account assertion to %s", accountAssertionPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// grepPattern extracts a specific pattern from a file
|
||||
func grepPattern(filePath, pattern string) string {
|
||||
content, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read from file %s: %v", filePath, err)
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, pattern) {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
encodedValue := strings.TrimSpace(parts[1])
|
||||
// Check if the value is base64 encoded
|
||||
if decodedBytes, err := base64.StdEncoding.DecodeString(encodedValue); err == nil {
|
||||
return string(decodedBytes)
|
||||
}
|
||||
return encodedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Fatalf("Pattern %s not found in file %s", pattern, filePath)
|
||||
return ""
|
||||
}
|
Loading…
Reference in new issue