github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/image/helpers.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package image 21 22 // TODO: put these in appropriate package(s) once they are clarified a bit more 23 24 import ( 25 "bytes" 26 "context" 27 "crypto" 28 "encoding/json" 29 "errors" 30 "fmt" 31 "io/ioutil" 32 "net/url" 33 "os" 34 "os/signal" 35 "path/filepath" 36 "strings" 37 "syscall" 38 39 "github.com/mvo5/goconfigparser" 40 41 "github.com/snapcore/snapd/asserts" 42 "github.com/snapcore/snapd/asserts/snapasserts" 43 "github.com/snapcore/snapd/gadget" 44 "github.com/snapcore/snapd/logger" 45 "github.com/snapcore/snapd/osutil" 46 "github.com/snapcore/snapd/overlord/auth" 47 "github.com/snapcore/snapd/progress" 48 "github.com/snapcore/snapd/release" 49 "github.com/snapcore/snapd/snap" 50 "github.com/snapcore/snapd/snapdenv" 51 "github.com/snapcore/snapd/store" 52 "github.com/snapcore/snapd/strutil" 53 ) 54 55 // A Store can find metadata on snaps, download snaps and fetch assertions. 56 type Store interface { 57 SnapAction(context.Context, []*store.CurrentSnap, []*store.SnapAction, store.AssertionQuery, *auth.UserState, *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) 58 Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error 59 60 Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) 61 } 62 63 // ToolingStore wraps access to the store for tools. 64 type ToolingStore struct { 65 sto Store 66 user *auth.UserState 67 } 68 69 func newToolingStore(arch, storeID string) (*ToolingStore, error) { 70 cfg := store.DefaultConfig() 71 cfg.Architecture = arch 72 cfg.StoreID = storeID 73 var user *auth.UserState 74 if authFn := os.Getenv("UBUNTU_STORE_AUTH_DATA_FILENAME"); authFn != "" { 75 var err error 76 user, err = readAuthFile(authFn) 77 if err != nil { 78 return nil, err 79 } 80 } 81 sto := store.New(cfg, toolingStoreContext{}) 82 return &ToolingStore{ 83 sto: sto, 84 user: user, 85 }, nil 86 } 87 88 type authData struct { 89 Macaroon string `json:"macaroon"` 90 Discharges []string `json:"discharges"` 91 } 92 93 func readAuthFile(authFn string) (*auth.UserState, error) { 94 data, err := ioutil.ReadFile(authFn) 95 if err != nil { 96 return nil, fmt.Errorf("cannot read auth file %q: %v", authFn, err) 97 } 98 99 creds, err := parseAuthFile(authFn, data) 100 if err != nil { 101 // try snapcraft login format instead 102 var err2 error 103 creds, err2 = parseSnapcraftLoginFile(authFn, data) 104 if err2 != nil { 105 trimmed := bytes.TrimSpace(data) 106 if len(trimmed) > 0 && trimmed[0] == '[' { 107 return nil, err2 108 } 109 return nil, err 110 } 111 } 112 113 return &auth.UserState{ 114 StoreMacaroon: creds.Macaroon, 115 StoreDischarges: creds.Discharges, 116 }, nil 117 } 118 119 func parseAuthFile(authFn string, data []byte) (*authData, error) { 120 var creds authData 121 err := json.Unmarshal(data, &creds) 122 if err != nil { 123 return nil, fmt.Errorf("cannot decode auth file %q: %v", authFn, err) 124 } 125 if creds.Macaroon == "" || len(creds.Discharges) == 0 { 126 return nil, fmt.Errorf("invalid auth file %q: missing fields", authFn) 127 } 128 return &creds, nil 129 } 130 131 func snapcraftLoginSection() string { 132 if snapdenv.UseStagingStore() { 133 return "login.staging.ubuntu.com" 134 } 135 return "login.ubuntu.com" 136 } 137 138 func parseSnapcraftLoginFile(authFn string, data []byte) (*authData, error) { 139 errPrefix := fmt.Sprintf("invalid snapcraft login file %q", authFn) 140 141 cfg := goconfigparser.New() 142 if err := cfg.ReadString(string(data)); err != nil { 143 return nil, fmt.Errorf("%s: %v", errPrefix, err) 144 } 145 sec := snapcraftLoginSection() 146 macaroon, err := cfg.Get(sec, "macaroon") 147 if err != nil { 148 return nil, fmt.Errorf("%s: %s", errPrefix, err) 149 } 150 unboundDischarge, err := cfg.Get(sec, "unbound_discharge") 151 if err != nil { 152 return nil, fmt.Errorf("%s: %v", errPrefix, err) 153 } 154 if macaroon == "" || unboundDischarge == "" { 155 return nil, fmt.Errorf("invalid snapcraft login file %q: empty fields", authFn) 156 } 157 return &authData{ 158 Macaroon: macaroon, 159 Discharges: []string{unboundDischarge}, 160 }, nil 161 } 162 163 // toolingStoreContext implements trivially store.DeviceAndAuthContext 164 // except implementing UpdateUserAuth properly to be used to refresh a 165 // soft-expired user macaroon. 166 type toolingStoreContext struct{} 167 168 func (tac toolingStoreContext) CloudInfo() (*auth.CloudInfo, error) { 169 return nil, nil 170 } 171 172 func (tac toolingStoreContext) Device() (*auth.DeviceState, error) { 173 return &auth.DeviceState{}, nil 174 } 175 176 func (tac toolingStoreContext) DeviceSessionRequestParams(_ string) (*store.DeviceSessionRequestParams, error) { 177 return nil, store.ErrNoSerial 178 } 179 180 func (tac toolingStoreContext) ProxyStoreParams(defaultURL *url.URL) (proxyStoreID string, proxySroreURL *url.URL, err error) { 181 return "", defaultURL, nil 182 } 183 184 func (tac toolingStoreContext) StoreID(fallback string) (string, error) { 185 return fallback, nil 186 } 187 188 func (tac toolingStoreContext) UpdateDeviceAuth(_ *auth.DeviceState, newSessionMacaroon string) (*auth.DeviceState, error) { 189 return nil, fmt.Errorf("internal error: no device state in tools") 190 } 191 192 func (tac toolingStoreContext) UpdateUserAuth(user *auth.UserState, discharges []string) (*auth.UserState, error) { 193 user.StoreDischarges = discharges 194 return user, nil 195 } 196 197 func NewToolingStoreFromModel(model *asserts.Model, fallbackArchitecture string) (*ToolingStore, error) { 198 architecture := model.Architecture() 199 // can happen on classic 200 if architecture == "" { 201 architecture = fallbackArchitecture 202 } 203 return newToolingStore(architecture, model.Store()) 204 } 205 206 func NewToolingStore() (*ToolingStore, error) { 207 arch := os.Getenv("UBUNTU_STORE_ARCH") 208 storeID := os.Getenv("UBUNTU_STORE_ID") 209 return newToolingStore(arch, storeID) 210 } 211 212 // DownloadOptions carries options for downloading snaps plus assertions. 213 type DownloadOptions struct { 214 TargetDir string 215 // if TargetPathFunc is not nil it will be invoked 216 // to compute the target path for the download and TargetDir is 217 // ignored 218 TargetPathFunc func(*snap.Info) (string, error) 219 220 Revision snap.Revision 221 Channel string 222 CohortKey string 223 Basename string 224 225 LeavePartialOnError bool 226 } 227 228 var ( 229 errRevisionAndCohort = errors.New("cannot specify both revision and cohort") 230 errPathInBase = errors.New("cannot specify a path in basename (use target dir for that)") 231 ) 232 233 func (opts *DownloadOptions) validate() error { 234 if strings.ContainsRune(opts.Basename, filepath.Separator) { 235 return errPathInBase 236 } 237 if !(opts.Revision.Unset() || opts.CohortKey == "") { 238 return errRevisionAndCohort 239 } 240 return nil 241 } 242 243 func (opts *DownloadOptions) String() string { 244 spec := make([]string, 0, 5) 245 if !opts.Revision.Unset() { 246 spec = append(spec, fmt.Sprintf("(%s)", opts.Revision)) 247 } 248 if opts.Channel != "" { 249 spec = append(spec, fmt.Sprintf("from channel %q", opts.Channel)) 250 } 251 if opts.CohortKey != "" { 252 // cohort keys are really long, and the rightmost bit being the 253 // interesting bit, so ellipt the rest 254 spec = append(spec, fmt.Sprintf(`from cohort %q`, strutil.ElliptLeft(opts.CohortKey, 10))) 255 } 256 if opts.Basename != "" { 257 spec = append(spec, fmt.Sprintf("to %q", opts.Basename+".snap")) 258 } 259 if opts.TargetDir != "" { 260 spec = append(spec, fmt.Sprintf("in %q", opts.TargetDir)) 261 } 262 return strings.Join(spec, " ") 263 } 264 265 // DownloadSnap downloads the snap with the given name and optionally 266 // revision using the provided store and options. It returns the final 267 // full path of the snap and a snap.Info for it and optionally a 268 // channel the snap got redirected to. 269 func (tsto *ToolingStore) DownloadSnap(name string, opts DownloadOptions) (targetFn string, info *snap.Info, redirectChannel string, err error) { 270 if err := opts.validate(); err != nil { 271 return "", nil, "", err 272 } 273 sto := tsto.sto 274 275 if opts.TargetPathFunc == nil && opts.TargetDir == "" { 276 pwd, err := os.Getwd() 277 if err != nil { 278 return "", nil, "", err 279 } 280 opts.TargetDir = pwd 281 } 282 283 if !opts.Revision.Unset() { 284 // XXX: is this really necessary (and, if it is, shoudn't we error out instead) 285 opts.Channel = "" 286 } 287 288 logger.Debugf("Going to download snap %q %s.", name, &opts) 289 290 actions := []*store.SnapAction{{ 291 Action: "download", 292 InstanceName: name, 293 Revision: opts.Revision, 294 CohortKey: opts.CohortKey, 295 Channel: opts.Channel, 296 }} 297 298 sars, _, err := sto.SnapAction(context.TODO(), nil, actions, nil, tsto.user, nil) 299 if err != nil { 300 // err will be 'cannot download snap "foo": <reasons>' 301 return "", nil, "", err 302 } 303 snap := sars[0].Info 304 redirectChannel = sars[0].RedirectChannel 305 306 if opts.TargetPathFunc == nil { 307 baseName := opts.Basename 308 if baseName == "" { 309 baseName = snap.Filename() 310 } else { 311 baseName += ".snap" 312 } 313 targetFn = filepath.Join(opts.TargetDir, baseName) 314 } else { 315 var err error 316 targetFn, err = opts.TargetPathFunc(snap) 317 if err != nil { 318 return "", nil, "", err 319 } 320 } 321 322 // check if we already have the right file 323 if osutil.FileExists(targetFn) { 324 sha3_384Dgst, size, err := osutil.FileDigest(targetFn, crypto.SHA3_384) 325 if err == nil && size == uint64(snap.DownloadInfo.Size) && fmt.Sprintf("%x", sha3_384Dgst) == snap.DownloadInfo.Sha3_384 { 326 logger.Debugf("not downloading, using existing file %s", targetFn) 327 return targetFn, snap, redirectChannel, nil 328 } 329 logger.Debugf("File exists but has wrong hash, ignoring (here).") 330 } 331 332 pb := progress.MakeProgressBar() 333 defer pb.Finished() 334 335 // Intercept sigint 336 c := make(chan os.Signal, 3) 337 signal.Notify(c, syscall.SIGINT) 338 go func() { 339 <-c 340 pb.Finished() 341 os.Exit(1) 342 }() 343 344 dlOpts := &store.DownloadOptions{LeavePartialOnError: opts.LeavePartialOnError} 345 if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, tsto.user, dlOpts); err != nil { 346 return "", nil, "", err 347 } 348 349 signal.Reset(syscall.SIGINT) 350 351 return targetFn, snap, redirectChannel, nil 352 } 353 354 // AssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them. 355 func (tsto *ToolingStore) AssertionFetcher(db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher { 356 retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) { 357 return tsto.sto.Assertion(ref.Type, ref.PrimaryKey, tsto.user) 358 } 359 save2 := func(a asserts.Assertion) error { 360 // for checking 361 err := db.Add(a) 362 if err != nil { 363 if _, ok := err.(*asserts.RevisionError); ok { 364 return nil 365 } 366 return fmt.Errorf("cannot add assertion %v: %v", a.Ref(), err) 367 } 368 return save(a) 369 } 370 return asserts.NewFetcher(db, retrieve, save2) 371 } 372 373 // FetchAndCheckSnapAssertions fetches and cross checks the snap assertions matching the given snap file using the provided asserts.Fetcher and assertion database. 374 func FetchAndCheckSnapAssertions(snapPath string, info *snap.Info, f asserts.Fetcher, db asserts.RODatabase) (*asserts.SnapDeclaration, error) { 375 sha3_384, size, err := asserts.SnapFileSHA3_384(snapPath) 376 if err != nil { 377 return nil, err 378 } 379 380 // this assumes series "16" 381 if err := snapasserts.FetchSnapAssertions(f, sha3_384); err != nil { 382 return nil, fmt.Errorf("cannot fetch snap signatures/assertions: %v", err) 383 } 384 385 // cross checks 386 if err := snapasserts.CrossCheck(info.InstanceName(), sha3_384, size, &info.SideInfo, db); err != nil { 387 return nil, err 388 } 389 390 a, err := db.Find(asserts.SnapDeclarationType, map[string]string{ 391 "series": release.Series, 392 "snap-id": info.SnapID, 393 }) 394 if err != nil { 395 return nil, fmt.Errorf("internal error: lost snap declaration for %q: %v", info.InstanceName(), err) 396 } 397 return a.(*asserts.SnapDeclaration), nil 398 } 399 400 // Find provides the snapsserts.Finder interface for snapasserts.DerviceSideInfo 401 func (tsto *ToolingStore) Find(at *asserts.AssertionType, headers map[string]string) (asserts.Assertion, error) { 402 pk, err := asserts.PrimaryKeyFromHeaders(at, headers) 403 if err != nil { 404 return nil, err 405 } 406 return tsto.sto.Assertion(at, pk, tsto.user) 407 } 408 409 // var so that it can be mocked for tests 410 var writeResolvedContent = writeResolvedContentImpl 411 412 // writeResolvedContent takes gadget.Info and the unpacked 413 // gadget/kernel snaps and outputs the resolved content from the 414 // {gadget,kernel}.yaml into a filesystem tree with the structure: 415 // <prepareImageDir>/resolved-content/<volume-name>/part<structure-nr>/... 416 // 417 // E.g. 418 // /tmp/prep-img/resolved-content/pi/part0/{config.txt,bootcode.bin,...} 419 func writeResolvedContentImpl(prepareDir string, info *gadget.Info, gadgetUnpackDir, kernelUnpackDir string) error { 420 fullPrepareDir, err := filepath.Abs(prepareDir) 421 if err != nil { 422 return err 423 } 424 targetDir := filepath.Join(fullPrepareDir, "resolved-content") 425 426 for volName, vol := range info.Volumes { 427 pvol, err := gadget.LayoutVolume(gadgetUnpackDir, kernelUnpackDir, vol, gadget.DefaultConstraints) 428 if err != nil { 429 return err 430 } 431 for i, ps := range pvol.LaidOutStructure { 432 if !ps.HasFilesystem() { 433 continue 434 } 435 mw, err := gadget.NewMountedFilesystemWriter(&ps, nil) 436 if err != nil { 437 return err 438 } 439 // ubuntu-image uses the "part{}" nomenclature 440 dst := filepath.Join(targetDir, volName, fmt.Sprintf("part%d", i)) 441 // on UC20, ensure system-seed links back to the 442 // <PrepareDir>/system-seed 443 if ps.Role == gadget.SystemSeed { 444 uc20systemSeedDir := filepath.Join(fullPrepareDir, "system-seed") 445 if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { 446 return err 447 } 448 if err := os.Symlink(uc20systemSeedDir, dst); err != nil { 449 return err 450 } 451 } 452 if err := mw.Write(dst, nil); err != nil { 453 return err 454 } 455 } 456 } 457 458 return nil 459 }