mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
This adds options --snap and --assertion to the "snap remodel" command, which can be used to pass local files to snapd so offline remodelling is possible.
218 lines
5.3 KiB
Go
218 lines
5.3 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2019 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* 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.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/textproto"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/snapcore/snapd/asserts"
|
|
)
|
|
|
|
type remodelData struct {
|
|
NewModel string `json:"new-model"`
|
|
}
|
|
|
|
// Remodel tries to remodel the system with the given assertion data
|
|
func (client *Client) Remodel(b []byte) (changeID string, err error) {
|
|
data, err := json.Marshal(&remodelData{
|
|
NewModel: string(b),
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot marshal remodel data: %v", err)
|
|
}
|
|
headers := map[string]string{
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
return client.doAsync("POST", "/v2/model", nil, headers, bytes.NewReader(data))
|
|
}
|
|
|
|
// RemodelOffline tries to remodel the system with the given model assertion
|
|
// and local snaps and assertion files.
|
|
func (client *Client) RemodelOffline(
|
|
model []byte, snapPaths, assertPaths []string) (changeID string, err error) {
|
|
|
|
// Check if all files exist before starting the go routine
|
|
snapFiles, err := checkAndOpenFiles(snapPaths)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
assertsFiles, err := checkAndOpenFiles(assertPaths)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
pr, pw := io.Pipe()
|
|
mw := multipart.NewWriter(pw)
|
|
go sendRemodelFiles(model, snapPaths, snapFiles, assertsFiles, pw, mw)
|
|
|
|
headers := map[string]string{
|
|
"Content-Type": mw.FormDataContentType(),
|
|
}
|
|
|
|
_, changeID, err = client.doAsyncFull("POST", "/v2/model", nil, headers, pr, doNoTimeoutAndRetry)
|
|
return changeID, err
|
|
}
|
|
|
|
func checkAndOpenFiles(paths []string) ([]*os.File, error) {
|
|
var files []*os.File
|
|
for _, path := range paths {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
for _, openFile := range files {
|
|
openFile.Close()
|
|
}
|
|
return nil, fmt.Errorf("cannot open %q: %w", path, err)
|
|
}
|
|
|
|
files = append(files, f)
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
func createAssertionPart(name string, mw *multipart.Writer) (io.Writer, error) {
|
|
h := make(textproto.MIMEHeader)
|
|
h.Set("Content-Disposition",
|
|
fmt.Sprintf(`form-data; name="%s"`, name))
|
|
h.Set("Content-Type", asserts.MediaType)
|
|
return mw.CreatePart(h)
|
|
}
|
|
|
|
func sendRemodelFiles(model []byte, paths []string, files, assertFiles []*os.File, pw *io.PipeWriter, mw *multipart.Writer) {
|
|
defer func() {
|
|
for _, f := range files {
|
|
f.Close()
|
|
}
|
|
}()
|
|
|
|
w, err := createAssertionPart("new-model", mw)
|
|
if err != nil {
|
|
pw.CloseWithError(err)
|
|
return
|
|
}
|
|
_, err = w.Write(model)
|
|
if err != nil {
|
|
pw.CloseWithError(err)
|
|
return
|
|
}
|
|
|
|
for _, file := range assertFiles {
|
|
if err := sendPartFromFile(file,
|
|
func() (io.Writer, error) {
|
|
return createAssertionPart("assertion", mw)
|
|
}); err != nil {
|
|
pw.CloseWithError(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
for i, file := range files {
|
|
if err := sendPartFromFile(file,
|
|
func() (io.Writer, error) {
|
|
return mw.CreateFormFile("snap", filepath.Base(paths[i]))
|
|
}); err != nil {
|
|
pw.CloseWithError(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
mw.Close()
|
|
pw.Close()
|
|
}
|
|
|
|
func sendPartFromFile(file *os.File, writeHeader func() (io.Writer, error)) error {
|
|
fw, err := writeHeader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = io.Copy(fw, file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CurrentModelAssertion returns the current model assertion
|
|
func (client *Client) CurrentModelAssertion() (*asserts.Model, error) {
|
|
assert, err := currentAssertion(client, "/v2/model")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
modelAssert, ok := assert.(*asserts.Model)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected assertion type (%s) returned", assert.Type().Name)
|
|
}
|
|
return modelAssert, nil
|
|
}
|
|
|
|
// CurrentSerialAssertion returns the current serial assertion
|
|
func (client *Client) CurrentSerialAssertion() (*asserts.Serial, error) {
|
|
assert, err := currentAssertion(client, "/v2/model/serial")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
serialAssert, ok := assert.(*asserts.Serial)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected assertion type (%s) returned", assert.Type().Name)
|
|
}
|
|
return serialAssert, nil
|
|
}
|
|
|
|
// helper function for getting assertions from the daemon via a REST path
|
|
func currentAssertion(client *Client, path string) (asserts.Assertion, error) {
|
|
q := url.Values{}
|
|
|
|
response, cancel, err := client.rawWithTimeout(context.Background(), "GET", path, q, nil, nil, nil)
|
|
if err != nil {
|
|
fmt := "failed to query current assertion: %w"
|
|
return nil, xerrors.Errorf(fmt, err)
|
|
}
|
|
defer cancel()
|
|
defer response.Body.Close()
|
|
if response.StatusCode != 200 {
|
|
return nil, parseError(response)
|
|
}
|
|
|
|
dec := asserts.NewDecoder(response.Body)
|
|
|
|
// only decode a single assertion - we can't ever get more than a single
|
|
// assertion through these endpoints by design
|
|
assert, err := dec.Decode()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode assertions: %v", err)
|
|
}
|
|
|
|
return assert, nil
|
|
}
|