-
Notifications
You must be signed in to change notification settings - Fork 590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
snap-bootstrap: load partition information from a manifest file for cloudimg-rootfs mode #14713
base: master
Are you sure you want to change the base?
Conversation
@sespiros probably it would be good to rebase this now that the prerequisite has been merged |
5c312d1
to
45d1ecd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sespiros I think we discussed this at the sprint, my strong preference would be to first move the existing CVM support to a _cvm.go and _cmv_test.go file, and then do this change
45d1ecd
to
b4d37ea
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #14713 +/- ##
==========================================
+ Coverage 78.20% 78.22% +0.01%
==========================================
Files 1151 1156 +5
Lines 151396 152997 +1601
==========================================
+ Hits 118402 119684 +1282
- Misses 25662 25919 +257
- Partials 7332 7394 +62
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
b4d37ea
to
6b717d2
Compare
This is a preparatory commit which modifies the existing CVM mode to support mounting multiple overlaid partitions for the rootfs.
In order to provide integrity protection for the root fs, the root fs will be mounted read-only and will be accompanied by dm-verity data from a new auto-discovered partition. To support writable state for the ephemeral CVM scenario, an overlayfs mount is created with an upper layer residing on tmpfs. A writable layer residing on disk can be also supported but it is currently disabled. The filesystem layout in the initramfs will look like: . /run/mnt ├── ... ├── data (where the final merged overlay will be mounted) ├── cloudimg-rootfs (read-only mount of the root fs, folder name is the fs label) ├── .. (future WIP, other partition mounts as separate mounts) └── cloudimg-rootfs-writable (tmpfs mount for the writable upper layer) ├── work └── upper An example final overlay mount command will look like: mount -t overlay overlay -olowerdir=/run/mnt/cloudimg-rootfs,\ upperdir=/run/mnt/cloudimg-rootfs-writable/upper,\ workdir=/run/mnt/cloudimg-rootfs-writable/work \ /run/mnt/data
…verity partition instead
v1 CVM images use the filesystem label to auto-discover an encrypted rootfs partition based on whether it has the '-enc' suffix. The new images use the GPT label to auto-discover the verity partition. In order to keep backwards compatibility, the customization tool will use set both labels to the same one. This commit clarifies that the field in the new structs actually contain GPT label information.
creating the upper and work folders used for setting up the overlayfs mount, was initially handled by snap-bootstrap whether it set up a tmpfs or detected a writable partition on disk but in order to support the writable partition case, it was later discovered that these folders might need to be created during the customization step and might already exist. snap-bootstrap will now create those folders only if it has detected no writable partition in the manifest and has auto-created a tmpfs-based writable partition.
trying to boot an image on a system without a TPM present was failing due to not-finding a TPM device. The source of the error was not the provision step as secbotProvisionForCVM already ignores errors (returns nil) if a TPM device is not found or a template file for provision the TPM is not found.
CVM mode used to mount the rootfs partition with the Ephemeral flag as cloud images contain a fixed fstab entry.
This is a legacy check that is not actually needed now as secbootUnlockVolumeUsingSealedKeyIfEncrypted will only find partitions residing on the disk that the ESP is from.
6b717d2
to
c4ab893
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I have some comments
Opts *systemdMountOptions | ||
} | ||
|
||
type ImageManifestPartition struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this and other new types like ImageManifest or ManifestError do not need to be exported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made them private.
cmd/snap-bootstrap/export_test.go
Outdated
@@ -94,6 +94,14 @@ func MockOsutilIsMounted(f func(string) (bool, error)) (restore func()) { | |||
} | |||
} | |||
|
|||
func MockOsReadFile(f func(string) ([]byte, error)) (restore func()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In tests we tend to create the files instead of mocking the read, note that dirs.GlobalRootDir
is set to a temporary folder so this is in principle easy to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented the suggestion, thanks.
GptLabel string `json:"label"` | ||
RootHash string `json:"root_hash"` | ||
Overlay string `json:"overlay"` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A description on what is each of these fields would be helpful, or maybe a link to a description of the format if available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added descriptions for all the proposed fields.
} | ||
|
||
type ImageManifest struct { | ||
Partitions []ImageManifestPartition `json:"partitions"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same: re describing the field
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a description although I feel it is a bit self-describing.
return im, nil | ||
} | ||
|
||
// generateMountsFromManifest is used to parse a manifest file which contains information about which |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
strictly speaking, the manifest passed around parsed already, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true, I modified my comment here.
if p.Overlay == "lowerdir" { | ||
// XXX: currently only one lower layer is permitted. The rest, if found, are ignored. | ||
if foundReadOnlyPartition != "" { | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should not this as a minimum be logged? Or should it be considered an error in the manifest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should at least log something here, yes. Initially I thought to support potential future cases where I might have an older snap-bootstrap (which only supports a single lowerdir for instance) be supplied with a manifest that might be richer in functionality (multiple lowerdirs) and still allow it to boot.
This might be too complicated of a use case so I decided to throw an error instead.
} else { | ||
// Only one writable partition is permitted. | ||
if foundWritablePartition != "" { | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same regarding log/error out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See previous comment. This will now be stricter and throw an error.
GptLabel: "cloudimg-rootfs", | ||
Opts: &systemdMountOptions{ | ||
Overlayfs: true, | ||
LowerDirs: []string{filepath.Join(boot.InitramfsRunMntDir, partitionMounts[0].GptLabel)}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is using partitionMounts[0]
assuming an order in the definitions of the partitions in the file? should this always be the read-only partition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this to use the foundReadOnlyPartition
variable. I also reworked some bits a little bit:
- I no longer require any order (dropped some sanity checks from
parseImageManifest
) - I renamed the
Overlay
field which was a bit confusing toReadOnly
. I described the change in thegenerateMountsFromManifest
comment.
pm.Opts.UpperDir = filepath.Join(boot.InitramfsRunMntDir, foundWritablePartition, "upper") | ||
pm.Opts.WorkDir = filepath.Join(boot.InitramfsRunMntDir, foundWritablePartition, "work") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be done when pm
is defined, splitting the initialization of Opts does not seem necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
restore = main.MockOsReadFile(func(path string) ([]byte, error) { | ||
// check attempt to read manifest from specific path | ||
c.Assert(path, Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/ubuntu", "manifest.json")) | ||
return []byte(fmt.Sprintf(`{"partitions":[{"label":"cloudimg-rootfs","root_hash":%q,"overlay":"lowerdir"}]}`, expectedRootHash)), nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need a test case for when more than one partition is defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I modified the existing one to accommodate the changes to the manifest and also added extra test cases for the private generateMountsFromManifest
to test multiple unsupported partitions scenarios.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did a pass
if err != nil { | ||
return err | ||
} | ||
if err := os.Mkdir(path, fi.Mode()); err != nil && !os.IsExist(err) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
won't we always get an IsExist error, otherwise how can the previous os.Stat work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reworked that bit.
return partitionMounts, nil | ||
} | ||
|
||
var createOverlayDirs = func(path string) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not tested
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added several test cases.
// then we need to specify that the data mountpoint is | ||
// expected to be a decrypted device | ||
diskOpts.IsDecryptedDevice = true | ||
if imageManifestFile != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this be an "else" of the previous "if"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is the same logically (could os.ReadFile
return both an error and non nil imageManifestFile?). I changed it and put it in the else block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the changes, I have some minor comments
} else { | ||
// Only one writable partition is permitted. | ||
if foundWritablePartition != "" { | ||
return nil, errors.New("manifest contains multiple non read-only partitions") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: maybe "manifest contains multiple writable partitions"
cmd/snap-bootstrap/export_test.go
Outdated
@@ -241,3 +247,11 @@ func MockBuildInstallObserver(f func(model *asserts.Model, gadgetDir string, use | |||
installBuildInstallObserver = old | |||
} | |||
} | |||
|
|||
func MockCreateOverlayDirs(f func(path string) error) (restore func()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My comment about mocking also applies here, you do not need to mock, you could use osutil.IsDirectory()
to check if the folder has been created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I replaced that with a call to osutil.IsDirectory()
.
// then we need to specify that the data mountpoint is | ||
// expected to be a decrypted device | ||
diskOpts.IsDecryptedDevice = true | ||
// XXX: if a manifest file is not found fall-back to CVM v1 behaviour |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: why the XXX in the comment? There is nothing to fix now or in the future here, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tagged with XXX the comments where either I made assumptions about which behavior would be actually most desirable (this case here) or where I foresee things will be changed. For this particular one, I believe this behavior is most likely what we will stick with so I removed the XXX.
pm.Where = filepath.Join(boot.InitramfsRunMntDir, p.GptLabel) | ||
|
||
if p.ReadOnly { | ||
// XXX: currently only a single read-only partition/overlay fs lowerdir is permitted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this something expected to change in the future? Maybe the comment should clarify this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have discussed about having support for multiple lowerdirs in the future to support varying degrees of customization. I clarified this in my comment.
// Create overlayfs' upperdir and workdir in the writable tmpfs layer. | ||
if pm.Opts.Tmpfs { | ||
if err := createOverlayDirs(pm.Where); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these directories then always expected to exist in a non-temporary writable partition? Maybe the comment could clarify this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I clarified the comment a bit for now. Currently we mostly care about the ephemeral case that doesn't have a persistent writable layer. If a persistent writable layer exists, I expect these directories to be created by the image creation tool and not by snap-bootstrap.
This was developed in conjunction with canonical/encrypt-cloud-image#26 (the image creation tool) which hasn't been reviewed yet. I might change this to make it actually a responsibility of snap-bootstrap once this gets some reviews.
// comes from the same disk as ESP | ||
return fmt.Errorf("cannot validate boot: cloudimg-rootfs mountpoint is expected to be from disk %s but is not", disk.Dev()) | ||
|
||
// Mount partitions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumes that the overlayfs mount is last, right? Maybe it could be good to add that to the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, generateMountsFromManifest
should create the partitionMounts
array in a specific order (ro, rw, overlayfs mount). I clarified the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
@sespiros there are some static checks failing atm |
Rebased on top of #14178 which is approved but not merged. New commits start at commit 2035d4d.Rebased on top of #14789 which is approved but not merged. New commits start at commit c55f509.This refactors cloudimg-rootfs mode and adds support for externally providing a dm-verity root hash for the rootfs that is mounted. This root hash is part of a new manifest file that resides on the ESP.
Subsequent PRs will add support for measuring the manifest to the TPM for remote attestation purposes.
Jira: https://warthogs.atlassian.net/browse/FR-9480