Skip to content

Commit 97f4f12

Browse files
authored
feat: add back support for mount points (#610)
This change adds back support for mounting volumes. This was initially done to support external kernel modules. Signed-off-by: Richard Case <richard.case@outlook.com>
1 parent 7aa5502 commit 97f4f12

File tree

14 files changed

+900
-27
lines changed

14 files changed

+900
-27
lines changed

api/services/microvm/v1alpha1/microvms.swagger.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,10 @@
481481
"type": "boolean",
482482
"description": "IsReadOnly specifies that the volume is to be mounted readonly."
483483
},
484+
"mountPoint": {
485+
"type": "string",
486+
"description": "MountPoint allows you to optionally specify a mount point for the volume. This only\napplied to additional volumes and it will use cloud-init to mount the volumes."
487+
},
484488
"source": {
485489
"$ref": "#/definitions/typesVolumeSource",
486490
"description": "Source is where the volume will be sourced from."

api/types/microvm.pb.go

Lines changed: 25 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/types/microvm.proto

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,13 @@ message StaticAddress {
128128

129129
// Volume represents the configuration for a volume to be attached to a microvm.
130130
message Volume {
131-
reserved 3;
132131
// ID is the uinique identifier of the volume.
133132
string id = 1;
134133
// IsReadOnly specifies that the volume is to be mounted readonly.
135134
bool is_read_only = 2;
135+
// MountPoint allows you to optionally specify a mount point for the volume. This only
136+
// applied to additional volumes and it will use cloud-init to mount the volumes.
137+
optional string mount_point = 3;
136138
// Source is where the volume will be sourced from.
137139
VolumeSource source = 4;
138140
// PartitionID is the uuid of the boot partition.

buf.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ deps:
44
- remote: buf.build
55
owner: googleapis
66
repository: googleapis
7-
commit: 783e4b5374fa488ab068d08af9658438
7+
commit: 75b4300737fb4efca0831636be94e517
88
- remote: buf.build
99
owner: grpc-ecosystem
1010
repository: grpc-gateway
11-
commit: b96615cde70c403f8075c48e56178f88
11+
commit: a1ecdc58eccd49aa8bea2a7a9022dc27

client/cloudinit/userdata/userdata.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,41 @@ type UserData struct {
1212
RunCommands []string `yaml:"runcmd,omitempty"`
1313
// BootCommands are commands you want to run early on in the boot process. These should only
1414
// be used for commands that are need early on and running them via RunCommands is too late.
15-
BootCommands []string `yaml:"bootcmd,omitempty"`
15+
BootCommands []string `yaml:"bootcmd,omitempty"`
16+
Mounts []Mount `yaml:"mounts,omitempty"`
17+
MountDefaultFields Mount `yaml:"mount_default_fields,omitempty,flow"`
1618
}
1719

20+
func (u *UserData) HasMountByName(deviceName string) bool {
21+
if len(u.Mounts) == 0 {
22+
return false
23+
}
24+
25+
for _, mount := range u.Mounts {
26+
if mount[0] == deviceName {
27+
return true
28+
}
29+
}
30+
31+
return false
32+
}
33+
34+
func (u *UserData) HasMountByMountPoint(mountPoint string) bool {
35+
if len(u.Mounts) == 0 {
36+
return false
37+
}
38+
39+
for _, mount := range u.Mounts {
40+
if mount[1] == mountPoint {
41+
return true
42+
}
43+
}
44+
45+
return false
46+
}
47+
48+
type Mount []string
49+
1850
type User struct {
1951
Name string `yaml:"name"`
2052
Sudo string `yaml:"sudo,omitempty"`

core/application/app_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ func createTestSpecWithMetadata(name, ns, uid string, metadata map[string]string
629629
{
630630
AllowMetadataRequests: true,
631631
GuestMAC: "AA:FF:00:00:00:01",
632-
GuestDeviceName: "mmds",
632+
GuestDeviceName: "eth0",
633633
Type: models.IfaceTypeTap,
634634
},
635635
{

core/application/commands.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
)
2020

2121
const (
22-
MetadataInterfaceName = "mmds"
22+
MetadataInterfaceName = "eth0"
2323
)
2424

2525
func (a *app) CreateMicroVM(ctx context.Context, mvm *models.MicroVM) (*models.MicroVM, error) {
@@ -192,15 +192,19 @@ func (a *app) addMetadataInterface(mvm *models.MicroVM) {
192192
}
193193
}
194194

195-
mvm.Spec.NetworkInterfaces = append(mvm.Spec.NetworkInterfaces, models.NetworkInterface{
196-
GuestDeviceName: MetadataInterfaceName,
197-
Type: models.IfaceTypeTap,
198-
AllowMetadataRequests: true,
199-
GuestMAC: "AA:FF:00:00:00:01",
200-
StaticAddress: &models.StaticAddress{
201-
Address: "169.254.0.1/16",
195+
interfaces := []models.NetworkInterface{
196+
{
197+
GuestDeviceName: MetadataInterfaceName,
198+
Type: models.IfaceTypeTap,
199+
AllowMetadataRequests: true,
200+
GuestMAC: "AA:FF:00:00:00:01",
201+
StaticAddress: &models.StaticAddress{
202+
Address: "169.254.0.1/16",
203+
},
202204
},
203-
})
205+
}
206+
interfaces = append(interfaces, mvm.Spec.NetworkInterfaces...)
207+
mvm.Spec.NetworkInterfaces = interfaces
204208

205209
return
206210
}

core/models/volumes.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ type Volume struct {
1212
PartitionID string `json:"partition_id,omitempty"`
1313
// Size is the size to resize this volume to.
1414
Size int32 `json:"size,omitempty"`
15+
// MountPoint allows you to optionally specify a mount point for the volume. This only
16+
// applied to additional volumes and it will use cloud-init to mount the volumes.
17+
MountPoint string `json:"mount_point,omitempty"`
1518
}
1619

1720
// Volumes represents a collection of volumes.
@@ -28,6 +31,18 @@ func (v Volumes) GetByID(id string) *Volume {
2831
return nil
2932
}
3033

34+
// HasMountableVolumes returns true if any of the volumes
35+
// have a mount point defined
36+
func (v Volumes) HasMountableVolumes() bool {
37+
for _, vol := range v {
38+
if vol.MountPoint != "" {
39+
return true
40+
}
41+
}
42+
43+
return false
44+
}
45+
3146
// VolumeSource is the source of a volume. Based loosely on the volumes in Kubernetes Pod specs.
3247
type VolumeSource struct {
3348
// Container is used to specify a source of a volume as a OCI container.

core/plans/microvm_create_update.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package plans
33
import (
44
"context"
55
"fmt"
6+
"github.com/weaveworks-liquidmetal/flintlock/core/steps/cloudinit"
67

78
"github.com/weaveworks-liquidmetal/flintlock/core/models"
89
"github.com/weaveworks-liquidmetal/flintlock/core/ports"
@@ -63,6 +64,11 @@ func (p *microvmCreateOrUpdatePlan) Create(ctx context.Context) ([]planner.Proce
6364
if err := p.addImageSteps(ctx, p.vm, ports.ImageService); err != nil {
6465
return nil, fmt.Errorf("adding image steps: %w", err)
6566
}
67+
if len(p.vm.Spec.AdditionalVolumes) > 0 {
68+
if err := p.addStep(ctx, cloudinit.NewDiskMountStep(p.vm)); err != nil {
69+
return nil, fmt.Errorf("adding mount step: %w", err)
70+
}
71+
}
6672

6773
// Network interfaces
6874
if err := p.addNetworkSteps(ctx, p.vm, ports.NetworkService); err != nil {

core/steps/cloudinit/disk_mount.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package cloudinit
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
8+
"github.com/sirupsen/logrus"
9+
"gopkg.in/yaml.v2"
10+
11+
"github.com/weaveworks-liquidmetal/flintlock/client/cloudinit"
12+
"github.com/weaveworks-liquidmetal/flintlock/client/cloudinit/userdata"
13+
"github.com/weaveworks-liquidmetal/flintlock/core/models"
14+
"github.com/weaveworks-liquidmetal/flintlock/pkg/log"
15+
"github.com/weaveworks-liquidmetal/flintlock/pkg/planner"
16+
)
17+
18+
func NewDiskMountStep(vm *models.MicroVM) planner.Procedure {
19+
return &diskMountStep{
20+
vm: vm,
21+
}
22+
}
23+
24+
type diskMountStep struct {
25+
vm *models.MicroVM
26+
}
27+
28+
// Name is the name of the procedure/operation.
29+
func (s *diskMountStep) Name() string {
30+
return "cloudinit_disk_mount"
31+
}
32+
33+
func (s *diskMountStep) ShouldDo(ctx context.Context) (bool, error) {
34+
logger := log.GetLogger(ctx).WithFields(logrus.Fields{
35+
"step": s.Name(),
36+
})
37+
logger.Debug("checking if procedure should be run")
38+
39+
if !s.vm.Spec.AdditionalVolumes.HasMountableVolumes() {
40+
return false, nil
41+
}
42+
43+
for _, vol := range s.vm.Spec.AdditionalVolumes {
44+
if vol.MountPoint == "" {
45+
continue
46+
}
47+
48+
status := s.vm.Status.Volumes[vol.ID]
49+
50+
if status == nil || status.Mount.Source == "" {
51+
return true, nil
52+
}
53+
}
54+
55+
vendorData, err := s.getVendorData()
56+
if err != nil {
57+
return false, fmt.Errorf("getting vendor data: %w", err)
58+
}
59+
if vendorData == nil {
60+
return true, nil
61+
}
62+
63+
for _, vol := range s.vm.Spec.AdditionalVolumes {
64+
if vol.MountPoint == "" {
65+
continue
66+
}
67+
68+
if !vendorData.HasMountByMountPoint(vol.MountPoint) {
69+
return true, nil
70+
}
71+
}
72+
73+
return false, nil
74+
}
75+
76+
// Do will perform the operation/procedure.
77+
func (s *diskMountStep) Do(ctx context.Context) ([]planner.Procedure, error) {
78+
logger := log.GetLogger(ctx).WithFields(logrus.Fields{
79+
"step": s.Name(),
80+
})
81+
logger.Debug("running step to mount additional disks via cloud-init")
82+
83+
vendorData, err := s.getVendorData()
84+
if err != nil {
85+
return nil, fmt.Errorf("getting vendor data: %w", err)
86+
}
87+
if vendorData == nil {
88+
vendorData = &userdata.UserData{}
89+
}
90+
91+
startingCode := int('b')
92+
for i, vol := range s.vm.Spec.AdditionalVolumes {
93+
if vol.MountPoint == "" {
94+
continue
95+
}
96+
97+
device := fmt.Sprintf("vd%c", rune(startingCode+i)) // Device number is always +1 as we have the root volume first
98+
99+
if !vendorData.HasMountByName(device) {
100+
vendorData.Mounts = append(vendorData.Mounts, userdata.Mount{
101+
device,
102+
vol.MountPoint,
103+
})
104+
}
105+
}
106+
vendorData.MountDefaultFields = userdata.Mount{"None", "None", "auto", "defaults,nofail", "0", "2"}
107+
108+
data, err := yaml.Marshal(vendorData)
109+
if err != nil {
110+
return nil, fmt.Errorf("marshalling vendor-data to yaml: %w", err)
111+
}
112+
dataWithHeader := append([]byte("## template: jinja\n#cloud-config\n\n"), data...)
113+
114+
if s.vm.Spec.Metadata == nil {
115+
s.vm.Spec.Metadata = map[string]string{}
116+
}
117+
s.vm.Spec.Metadata[cloudinit.VendorDataKey] = base64.StdEncoding.EncodeToString(dataWithHeader)
118+
119+
return nil, nil
120+
}
121+
122+
func (s *diskMountStep) Verify(ctx context.Context) error {
123+
return nil
124+
}
125+
126+
func (s *diskMountStep) getVendorData() (*userdata.UserData, error) {
127+
vendorDataRaw, ok := s.vm.Spec.Metadata[cloudinit.VendorDataKey]
128+
if !ok {
129+
return nil, nil
130+
}
131+
132+
vendorData := &userdata.UserData{}
133+
data, err := base64.StdEncoding.DecodeString(vendorDataRaw)
134+
if err != nil {
135+
return nil, fmt.Errorf("decoding vendor data: %w", err)
136+
}
137+
if marshalErr := yaml.Unmarshal(data, vendorData); marshalErr != nil {
138+
return nil, fmt.Errorf("unmarshalling vendor-data yaml: %w", err)
139+
}
140+
141+
return vendorData, nil
142+
}

0 commit comments

Comments
 (0)