Compare commits

...

11 Commits

9 changed files with 222 additions and 55 deletions

1
debian/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
files

32
debian/changelog vendored
View File

@ -1,3 +1,35 @@
snapd-extra-utils (1.1.1) plucky; urgency=medium
* Split apart autopkgtests to prevent neutral on some arches.
-- Simon Quigley <tsimonq2@ubuntu.com> Fri, 21 Feb 2025 13:45:49 -0600
snapd-extra-utils (1.1.0) plucky; urgency=medium
* [snapd-seed-glue] Fix installation of snaps in Calamares Full Installation
due to a missing account key, and add an autopkgtest to ensure we catch
this ahead of time in the future (LP: #2096649).
-- Simon Quigley <tsimonq2@ubuntu.com> Thu, 20 Feb 2025 14:09:13 -0600
snapd-extra-utils (1.0.7) plucky; urgency=medium
* Add several extra Breaks/Replaces to be safe.
-- Simon Quigley <tsimonq2@ubuntu.com> Wed, 11 Dec 2024 22:18:42 -0600
snapd-extra-utils (1.0.6) plucky; urgency=medium
* [autopkgtest] Further refactoring to properly run the autopkgtest.
-- Simon Quigley <tsimonq2@ubuntu.com> Mon, 25 Nov 2024 01:57:44 -0600
snapd-extra-utils (1.0.5) plucky; urgency=medium
* [autopkgtest] Convert the snapd-seed-glue autopkgtest to C++.
-- Simon Quigley <tsimonq2@ubuntu.com> Sun, 24 Nov 2024 23:12:13 -0600
snapd-extra-utils (1.0.4) plucky; urgency=medium
* [autopkgtest] Use snaps that are available on all arches, to fix armhf,

4
debian/control vendored
View File

@ -17,6 +17,8 @@ Vcs-Git: https://git.lubuntu.me/Lubuntu/snapd-extra-utils.git
Package: snapd-seed-glue
Architecture: any
Depends: snapd, xdelta3, ${misc:Depends}, ${shlibs:Depends}
Breaks: calamares-settings-ubuntu-common (<< 1:25.04.1)
Replaces: calamares-settings-ubuntu-common (<< 1:25.04.1)
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
@ -25,6 +27,8 @@ Description: Installer and pre-seed utilities for snapd
Package: snapd-installation-monitor
Architecture: any
Depends: snapd, ${misc:Depends}, ${shlibs:Depends}
Breaks: lubuntu-snap-installation-monitor
Replaces: lubuntu-snap-installation-monitor
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,

4
debian/rules vendored
View File

@ -3,10 +3,12 @@ export DH_VERBOSE = 1
export GO111MODULE = off
export GOPATH=/usr/share/gocode
export GOCACHE=$(CURDIR)/.gocache
export NUM_CPUS = $(shell nproc)
%:
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)
(cd snapd-seed-glue/tests && cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo . && make -j${NUM_CPUS})
(cd snapd-installation-monitor && cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo . && make -j${NUM_CPUS})

12
debian/tests/control vendored
View File

@ -1,3 +1,9 @@
Tests: snapd-seed-glue
Depends: snapd-seed-glue
Restrictions: needs-internet
Test-Command: snapd-seed-glue/tests/snapd_seed_glue_test
Depends: @builddeps@, snapd-seed-glue
Restrictions: needs-internet, build-needed
Architecture: !amd64
Test-Command: snapd-seed-glue/tests/snapd_seed_glue_test
Depends: @builddeps@, snapd-seed-glue, livecd-rootfs, squashfs-tools
Restrictions: needs-internet, build-needed, isolation-machine, needs-sudo
Architecture: amd64

View File

@ -1,40 +0,0 @@
#!/bin/bash
set -e
# File to track command output, so we can check for successful completion
OUTPUT_FILE=$(mktemp)
# If the command output does not contain a final confirmation of success, exit 1
confirm_success () {
if grep -q "Cleanup and validation completed" "$OUTPUT_FILE"; then
rm -f "$OUTPUT_FILE"
else
exit 1
fi
}
# Function to run snapd-seed-glue with given arguments
run_snapd_seed_glue () {
/usr/bin/snapd-seed-glue --verbose --seed hello_test "$@" > >(tee -a "$OUTPUT_FILE") 2>&1
confirm_success
}
echo "[snapd-seed-glue autopkgtest] Testing snapd-seed-glue with hello..."
run_snapd_seed_glue hello
echo "[snapd-seed-glue autopkgtest] Add htop to the same seed..."
run_snapd_seed_glue hello htop
echo "[snapd-seed-glue autopkgtest] Remove htop and replace it with btop..."
run_snapd_seed_glue hello btop
echo "[snapd-seed-glue autopkgtest] Confirm that non-existent snaps will fail..."
/usr/bin/snapd-seed-glue --verbose --seed test_dir absolutelyridiculouslongnamethatwilldefinitelyneverexist > >(tee -a "$OUTPUT_FILE") 2>&1 || echo "Fail expected"
if ! grep -q "cannot install snap \"absolutelyridiculouslongnamethatwilldefinitelyneverexist\": snap not found" "$OUTPUT_FILE"; then
exit 1
fi

View File

@ -73,25 +73,45 @@ func downloadAssertions(storeClient *store.Store, snapInfo *snap.Info, downloadD
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
// Step 4: 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 5: Fetch account assertions
publisherAccountAssertion, err := storeClient.Assertion(assertionTypes["account"], []string{publisherID}, nil)
if err != nil {
return fmt.Errorf("failed to fetch developer account assertion for snap %s: %w", snapInfo.SuggestedName, err)
}
// Step 5.1: Determine authority account from snap-declaration
authorityID, ok := snapDecl.Header("authority-id").(string)
if !ok || authorityID == "" {
return fmt.Errorf("snap-declaration assertion missing 'authority-id' header for snap %s", snapInfo.SuggestedName)
}
// Step 5.2: Fetch authority account assertion
authorityAccountAssertion, err := storeClient.Assertion(assertionTypes["account"], []string{authorityID}, nil)
if err != nil {
return fmt.Errorf("failed to fetch authority account assertion for snap %s: %w", snapInfo.SuggestedName, err)
}
// Step 5.3: Fetch developer account assertion
developerID, ok := snapRevisionAssertion.Header("developer-id").(string)
developerAccountAssertion := authorityAccountAssertion
if ok && authorityID != "" {
developerAccountAssertion, err = storeClient.Assertion(assertionTypes["account"], []string{developerID}, nil)
if err != nil {
return fmt.Errorf("failed to fetch developer account assertion for snap %s: %w", snapInfo.SuggestedName, err)
}
}
// Step 6: Write assertions in the desired order
@ -99,12 +119,20 @@ func downloadAssertions(storeClient *store.Store, snapInfo *snap.Info, downloadD
writeAssertion("account-key", accountKeyAssertion, assertionsFile)
// 2. account
writeAssertion("account", accountAssertion, assertionsFile)
writeAssertion("account", publisherAccountAssertion, assertionsFile)
if authorityID != publisherID {
writeAssertion("account", authorityAccountAssertion, assertionsFile)
}
// 3. snap-declaration
writeAssertion("snap-declaration", snapDecl, assertionsFile)
// 4. snap-revision (if fetched successfully)
// 4. developer account if present
if developerAccountAssertion != authorityAccountAssertion {
writeAssertion("account", developerAccountAssertion, assertionsFile)
}
// 5. snap-revision (if fetched successfully)
if snapRevisionAssertion != nil {
writeAssertion("snap-revision", snapRevisionAssertion, assertionsFile)
}
@ -138,11 +166,13 @@ func writeAssertion(assertionType string, assertion asserts.Assertion, file *os.
body := assertion.Body()
bodyLength := len(body)
headers := assertion.Headers()
verboseLog("Assertion headers: %v", headers)
// Only write the account assertion if it is not Canonical
if assertionType == "account" {
value, exists := headers["username"]
if exists && value == "canonical" {
verboseLog("Skipping assertion due to duplication in account file")
return
}
}

View File

@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.16)
project(snapd_seed_glue_test)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_executable(snapd_seed_glue_test cli-tests.cpp)

View File

@ -0,0 +1,125 @@
// Copyright (C) 2024-2025 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 <array>
#include <cstdio>
#include <cstdlib>
#include <format>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#if defined(__x86_64__) || defined(__amd64__)
#include <filesystem>
#include <unistd.h>
#endif
std::string OUTPUT_FILE_CONTENTS;
std::pair<std::string, int> execute_command(const std::string& cmd) {
std::array<char, 128> buffer{};
std::string result;
// Redirect stderr to stdout
std::string cmd_with_redirect = cmd + " 2>&1";
FILE* pipe = popen(cmd_with_redirect.c_str(), "r");
if (!pipe) throw std::runtime_error("popen() failed!");
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
result += buffer.data();
// Also echo the output to stdout
std::cout << buffer.data();
}
int rc = pclose(pipe);
int exit_code = WEXITSTATUS(rc);
return {result, exit_code};
}
void confirm_success() {
if (OUTPUT_FILE_CONTENTS.find("Cleanup and validation completed") != std::string::npos) OUTPUT_FILE_CONTENTS.clear();
else exit(1);
}
void run_snapd_seed_glue(const std::vector<std::string>& args, const std::string& dir = "") {
std::string cmd;
if (!dir.empty()) cmd = std::format("snapd-seed-glue/snapd-seed-glue --verbose --seed {}", dir);
else cmd = "snapd-seed-glue/snapd-seed-glue --verbose --seed hello_test";
for (const auto& arg : args) cmd += " " + arg;
auto [output, exit_code] = execute_command(cmd);
OUTPUT_FILE_CONTENTS += output;
if (exit_code != 0) exit(exit_code);
confirm_success();
}
#if defined(__x86_64__) || defined(__amd64__)
std::string get_version_codename() {
std::ifstream file("/etc/os-release");
if (!file.is_open()) return {};
std::string line;
constexpr std::string_view key = "VERSION_CODENAME=";
while (std::getline(file, line)) if (line.starts_with(key)) return line.substr(key.size());
return "";
}
#endif
int main() {
std::cout << "[snapd-seed-glue autopkgtest] Testing snapd-seed-glue with hello...\n";
run_snapd_seed_glue({"hello"});
std::cout << "[snapd-seed-glue autopkgtest] Add htop to the same seed...\n";
run_snapd_seed_glue({"hello", "htop"});
std::cout << "[snapd-seed-glue autopkgtest] Remove htop and replace it with btop...\n";
run_snapd_seed_glue({"hello", "btop"});
std::cout << "[snapd-seed-glue autopkgtest] Confirm that non-existent snaps will fail...\n";
std::string invalid_snap = "absolutelyridiculouslongnamethatwilldefinitelyneverexist";
std::string cmd = "/usr/bin/snapd-seed-glue --verbose --seed test_dir " + invalid_snap;
auto [output, exit_code] = execute_command(cmd);
OUTPUT_FILE_CONTENTS += output;
if (exit_code != 0) std::cout << "Fail expected\n";
if (OUTPUT_FILE_CONTENTS.find("cannot install snap \"" + invalid_snap + "\": snap not found") == std::string::npos) exit(1);
#if defined(__x86_64__) || defined(__amd64__)
std::cout << "[snapd-seed-glue autopkgtest] Confirm that a livefs can be created, and snapd-seed-glue can be used...\n";
// Logic taken from lp:launchpad-buildd/lpbuildd/target/build_livefs.py
setenv("PROJECT", "lubuntu", 1);
setenv("ARCH", "amd64", 1);
setenv("SUITE", get_version_codename().c_str(), 1);
if (!std::filesystem::create_directory("auto")) exit(1);
for (std::string lb_script : {"config", "build", "clean"}) {
auto [link_output, link_exit_code] = execute_command(std::format("ln -s /usr/share/livecd-rootfs/live-build/auto/{} auto/", lb_script));
if (link_exit_code != 0) exit(link_exit_code);
}
auto [clean_output, clean_exit_code] = execute_command("sudo -E lb clean --purge");
if (clean_exit_code != 0) exit(clean_exit_code);
auto [config_output, config_exit_code] = execute_command("lb config");
if (config_exit_code != 0) exit(config_exit_code);
auto [build_output, build_exit_code] = execute_command("sudo -E lb build");
if (build_exit_code != 0) exit(build_exit_code);
auto [unsquashfs_output, unsquashfs_exit_code] = execute_command("sudo unsquashfs livecd.lubuntu.minimal.standard.squashfs /var/lib/snapd/seed/");
if (unsquashfs_exit_code != 0) exit(unsquashfs_exit_code);
auto [clean2_output, clean2_exit_code] = execute_command("sudo -E lb clean --purge");
if (clean2_exit_code != 0) exit(clean2_exit_code);
auto [chown_output, chown_exit_code] = execute_command(std::format("sudo chown -R {}:{} squashfs-root/", getuid(), getgid()));
if (chown_exit_code != 0) exit(chown_exit_code);
std::cout << "[snapd-seed-glue autopkgtest] A livefs can be created. Confirm snapd-seed-glue can be used...\n";
run_snapd_seed_glue({"firefox", "firmware-updater"}, "squashfs-root/var/lib/snapd/seed/");
run_snapd_seed_glue({"firefox", "firmware-updater", "krita", "thunderbird"}, "squashfs-root/var/lib/snapd/seed/");
#endif
return 0;
}