Compare commits

...

7 Commits

7 changed files with 139 additions and 41 deletions

1
debian/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
files

20
debian/changelog vendored
View File

@ -1,3 +1,23 @@
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.

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,

View File

@ -1,3 +1,9 @@
Test-Command: snapd-seed-glue/tests/snapd_seed_glue_test
Depends: snapd-seed-glue, tree
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

@ -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

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.14)
cmake_minimum_required(VERSION 3.16)
project(snapd_seed_glue_test)
set(CMAKE_CXX_STANDARD 23)

View File

@ -1,4 +1,4 @@
// Copyright (C) 2024 Simon Quigley <tsimonq2@ubuntu.com>
// 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
@ -10,14 +10,21 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <cstdio>
#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;
@ -39,28 +46,33 @@ std::pair<std::string, int> execute_command(const std::string& cmd) {
}
void confirm_success() {
if (OUTPUT_FILE_CONTENTS.find("Cleanup and validation completed") != std::string::npos) {
// Success, clear OUTPUT_FILE_CONTENTS
OUTPUT_FILE_CONTENTS.clear();
} else {
exit(1);
}
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) {
std::string cmd = "snapd-seed-glue/snapd-seed-glue --verbose --seed hello_test";
for (const auto& arg : args) {
cmd += " " + arg;
}
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);
// Append output to OUTPUT_FILE_CONTENTS
OUTPUT_FILE_CONTENTS += output;
if (exit_code != 0) {
exit(1);
}
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"});
@ -76,13 +88,38 @@ int main() {
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) {
// Expected error message found
} else {
exit(1);
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;
}