Merge lp:~julian-edwards/gwacl/vhd-define into lp:gwacl

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: 128
Merged at revision: 133
Proposed branch: lp:~julian-edwards/gwacl/vhd-define
Merge into: lp:gwacl
Diff against target: 202 lines (+151/-1)
4 files modified
Makefile (+1/-1)
storage.go (+80/-0)
storage_test.go (+69/-0)
vhd_footer.go (+1/-0)
To merge this branch: bzr merge lp:~julian-edwards/gwacl/vhd-define
Reviewer Review Type Date Requested Status
Gavin Panella Approve
Review via email: mp+170746@code.launchpad.net

Commit message

Add a convenience function CreateVHD to create a VHD based on a filesystem image.

Description of the change

Add a convenience function to create a VHD based on a filesystem image. It works by making a page blob of a fixed 20Mb size and uploading a fixed VHD footer of 512 bytes to the last page. The supplied filesystem image is then uploaded from the start of the blob. The resulting blob is then able to be mounted as an Azure disk on an instance.

The point of all this is to enable a callsite to make arbitrary data available to the new instance, since Azure does not provide any other way.

This branch probably (read: definitely) needs more error testing, but quite frankly I find writing test code in Go to be utter drudgery and have lost the will to live. If you have any specific and useful ideas to help me out, go for it.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good, but I'm concerned about [3]. I'm not 100% sure that's a
bug, so Needs Information.

[1]

+    Size           int       // How many bytes from the Filesystem data to
+                             // upload.  *Must* be a modulo of 512.

I'm not sure modulo is the right word here (and I don't think it's a
noun either). How about s/modulo/multiple/?

[2]

+    if err != nil {
+        // This really shouldn't ever happen since there's a test to make sure
+        // it can be decoded.
+        panic("Unable to base64 decode VHD_FOOTER")
+    }

We lose the original error here. How about panic(err) instead?

[3]

+    err = context.PutPage(&PutPageRequest{
+        Container:  req.Container,
+        Filename:   req.Filename,
+        StartRange: VHD_SIZE-513,
+        EndRange:   VHD_SIZE-1,
+        Data:       dataReader,
+    })

This should probably use len(data) instead of literals.

Also, this means that the maximum size of the filesystem data is
VHD_SIZE - len(data), not VHD_SIZE.

[4]

+func (context *StorageContext) CreateVHD(req *CreateVHDRequest) error {

This isn't a general purpose function for creating a VHD, so I think
it ought to be renamed to, say, CreateVHDForUserData or something like
that.

review: Needs Information
lp:~julian-edwards/gwacl/vhd-define updated
126. By Julian Edwards

tweaks and review comments

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 21/06/13 18:58, Gavin Panella wrote:
> Review: Needs Information
>
> Looks good, but I'm concerned about [3]. I'm not 100% sure that's a
> bug, so Needs Information.
>
>
> [1]
>
> + Size int // How many bytes from the Filesystem data to
> + // upload. *Must* be a modulo of 512.
>
> I'm not sure modulo is the right word here (and I don't think it's a
> noun either). How about s/modulo/multiple/?

It seems perfectly cromulent to me. But I'll change it since I don't
want to trip people up deliberately.

> [2]
>
> + if err != nil {
> + // This really shouldn't ever happen since there's a test to make sure
> + // it can be decoded.
> + panic("Unable to base64 decode VHD_FOOTER")
> + }
>
> We lose the original error here. How about panic(err) instead?

Yeah, probably. But this should never happen, right :)

>
>
> [3]
>
> + err = context.PutPage(&PutPageRequest{
> + Container: req.Container,
> + Filename: req.Filename,
> + StartRange: VHD_SIZE-513,
> + EndRange: VHD_SIZE-1,
> + Data: dataReader,
> + })
>
> This should probably use len(data) instead of literals.

No - Start/EndRange is a target position in the blob and the data has to
be uploaded to this exact page at the end of the blob. These are not
sizes, they're positions.

> Also, this means that the maximum size of the filesystem data is
> VHD_SIZE - len(data), not VHD_SIZE.

That's fine. While I did mean to add 512 bytes to the size it's not a
problem in practice. You still get a big disk.

>
>
> [4]
>
> +func (context *StorageContext) CreateVHD(req *CreateVHDRequest) error {
>
> This isn't a general purpose function for creating a VHD, so I think
> it ought to be renamed to, say, CreateVHDForUserData or something like
> that.

I see what you mean. I didn't want to mention user data in gwacl though
as that's a cloud-init term and gwacl is agnostic. Mmmmm.

Perhaps CreateInstanceDataVHD? I'll embellish the [doc] comment as well.

Revision history for this message
Gavin Panella (allenap) wrote :

> > [3]
> >
> > +    err = context.PutPage(&PutPageRequest{
> > +        Container:  req.Container,
> > +        Filename:   req.Filename,
> > +        StartRange: VHD_SIZE-513,
> > +        EndRange:   VHD_SIZE-1,
> > +        Data:       dataReader,
> > +    })
> >
> > This should probably use len(data) instead of literals.
>
> No - Start/EndRange is a target position in the blob and the data has to
> be uploaded to this exact page at the end of the blob.  These are not
> sizes, they're positions.

Agreed. However, StartRange needs to be len(data) before EndRange.

>
> > Also, this means that the maximum size of the filesystem data is
> > VHD_SIZE - len(data), not VHD_SIZE.
>
> That's fine.  While I did mean to add 512 bytes to the size it's not a
> problem in practice.  You still get a big disk.

It's pertinent in documentation and in validation. For example,
req.Size must not be greater than (VHD_SIZE - len(data)) or you'll
overwrite the footer.

review: Approve
lp:~julian-edwards/gwacl/vhd-define updated
127. By Julian Edwards

Add comments explaining the exact VHD size and fix doc comment to reflect what size data can be attached

128. By Julian Edwards

Make sure data cannot overwrite the vhd footer

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 24/06/13 20:22, Gavin Panella wrote:
> Review: Approve
>
>>> [3]
>>>
>>> + err = context.PutPage(&PutPageRequest{
>>> + Container: req.Container,
>>> + Filename: req.Filename,
>>> + StartRange: VHD_SIZE-513,
>>> + EndRange: VHD_SIZE-1,
>>> + Data: dataReader,
>>> + })
>>>
>>> This should probably use len(data) instead of literals.
>>
>> No - Start/EndRange is a target position in the blob and the data has to
>> be uploaded to this exact page at the end of the blob. These are not
>> sizes, they're positions.
>
> Agreed. However, StartRange needs to be len(data) before EndRange.

len(data) is always 512. It has to be, or the footer is broken. I
don't want to make StartRange len(data) because if the data is ever
broken this stuff won't fail in an obvious way. If I do use len(data)
then in the case of the footer being >512 it will either fail on an
invalid page range, or upload to more than one page which will succeed
here but later incomprehensibly fail when adding the disk to the VM.

I can understand where you are coming from with this, but I feel that
len(data) is not as declarative of intent as the way I've done it, as to
a casual reader it could look like we're pushing to more than one page
of data with some arbitrary length footer data.

>>> Also, this means that the maximum size of the filesystem data is
>>> VHD_SIZE - len(data), not VHD_SIZE.
>>
>> That's fine. While I did mean to add 512 bytes to the size it's not a
>> problem in practice. You still get a big disk.
>
> It's pertinent in documentation and in validation. For example,
> req.Size must not be greater than (VHD_SIZE - len(data)) or you'll
> overwrite the footer.

Ah! You're right about that, I'll fix comments/docs and add an extra
fix/test to make sure it can't happen.

Cheers.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2013-04-30 21:50:12 +0000
3+++ Makefile 2013-06-25 01:21:23 +0000
4@@ -1,6 +1,6 @@
5 # Build, and run tests.
6 check: examples
7- go test ./...
8+ go test -gocheck.v=true ./...
9
10 all_source := $(shell find . -name '*.go' ! -name '*_test.go')
11
12
13=== modified file 'storage.go'
14--- storage.go 2013-06-11 16:33:25 +0000
15+++ storage.go 2013-06-25 01:21:23 +0000
16@@ -8,6 +8,8 @@
17
18 import (
19 "bytes"
20+ "encoding/base64"
21+ "fmt"
22 "io"
23 . "launchpad.net/gwacl/logging"
24 "strconv"
25@@ -114,3 +116,81 @@
26 batch.Containers = containers
27 return batch, nil
28 }
29+
30+type CreateVHDRequest struct {
31+ Container string // Container name in the storage account
32+ Filename string // Specify the filename in which to store the VHD
33+ FilesystemData io.Reader // A formatted filesystem, e.g. iso9660.
34+ Size int // How many bytes from the Filesystem data to
35+ // upload. *Must* be a multiple of 512.
36+}
37+
38+// CreateInstanceDataVHD will take the supplied filesystem data and create an
39+// Azure VHD in a page blob out of it. The data cannot be bigger than
40+// gwacl.VHD_SIZE-512. This is intended to be used as a way of passing
41+// arbitrary data to a new instance - create a disk here and then attach it to
42+// the new instance.
43+func (context *StorageContext) CreateInstanceDataVHD(req *CreateVHDRequest) error {
44+ // We need several steps:
45+ // 1. Create an empty page blob of exactly VHD_SIZE bytes (see
46+ // vhd_footer.go)
47+ // 2. Upload VHD_FOOTER to the last page of the blob.
48+ // 3. Upload the supplied FilesystemData from the start of the blob.
49+
50+ var err error
51+
52+ if req.Size % 512 != 0 {
53+ return fmt.Errorf("Size must be a multiple of 512")
54+ }
55+ if req.Size > VHD_SIZE-512 {
56+ // Protect against writing over the VHD footer.
57+ return fmt.Errorf("Size cannot be bigger than %d", VHD_SIZE-512)
58+ }
59+
60+ // Step 1.
61+ err = context.PutBlob(&PutBlobRequest{
62+ Container: req.Container,
63+ BlobType: "page",
64+ Filename: req.Filename,
65+ Size: VHD_SIZE,
66+ })
67+
68+ if err != nil {
69+ return err
70+ }
71+
72+ // Step 2.
73+ data, err := base64.StdEncoding.DecodeString(VHD_FOOTER)
74+ if err != nil {
75+ // This really shouldn't ever happen since there's a test to make sure
76+ // it can be decoded.
77+ panic(err)
78+ }
79+ dataReader := bytes.NewReader(data)
80+ err = context.PutPage(&PutPageRequest{
81+ Container: req.Container,
82+ Filename: req.Filename,
83+ StartRange: VHD_SIZE-513, // last page of the blob
84+ EndRange: VHD_SIZE-1,
85+ Data: dataReader,
86+ })
87+
88+ if err != nil {
89+ return err
90+ }
91+
92+ // Step 3.
93+ err = context.PutPage(&PutPageRequest{
94+ Container: req.Container,
95+ Filename: req.Filename,
96+ StartRange: 0,
97+ EndRange: req.Size-1,
98+ Data: req.FilesystemData,
99+ })
100+
101+ if err != nil {
102+ return err
103+ }
104+
105+ return nil
106+}
107
108=== modified file 'storage_test.go'
109--- storage_test.go 2013-06-20 07:31:25 +0000
110+++ storage_test.go 2013-06-25 01:21:23 +0000
111@@ -5,6 +5,7 @@
112
113 import (
114 "bytes"
115+ "encoding/base64"
116 "fmt"
117 "io/ioutil"
118 . "launchpad.net/gocheck"
119@@ -333,3 +334,71 @@
120 c.Check(containers.Containers[0].Name, Equals, firstContainer)
121 c.Check(containers.Containers[1].Name, Equals, lastContainer)
122 }
123+
124+type testCreateInstanceDataVHD struct{}
125+
126+var _ = Suite(&testCreateInstanceDataVHD{})
127+
128+func (s *testCreateInstanceDataVHD) TestHappyPath(c *C) {
129+ response := http.Response{
130+ Status: fmt.Sprintf("%d", http.StatusOK),
131+ StatusCode: http.StatusCreated,
132+ }
133+ transport := &TestTransport2{}
134+ transport.AddExchange(&response, nil) // putblob response
135+ transport.AddExchange(&response, nil) // first putpage
136+ transport.AddExchange(&response, nil) // second putpage
137+ context := makeStorageContext(transport)
138+
139+ randomData := MakeRandomByteSlice(512)
140+ dataReader := bytes.NewReader(randomData)
141+
142+ var err error
143+
144+ err = context.CreateInstanceDataVHD(&CreateVHDRequest{
145+ Container: "container", Filename: "filename",
146+ FilesystemData: dataReader, Size: 512})
147+ c.Assert(err, IsNil)
148+
149+ // Check the PutBlob exchange.
150+ c.Check(
151+ transport.Exchanges[0].Request.Header.Get("x-ms-blob-type"),
152+ Equals, "PageBlob")
153+ expected_size := fmt.Sprintf("%d", VHD_SIZE)
154+ c.Check(
155+ transport.Exchanges[0].Request.Header.Get("x-ms-blob-content-length"),
156+ Equals, expected_size)
157+
158+ // Check the PutPage for the footer exchange.
159+ data, err := ioutil.ReadAll(transport.Exchanges[1].Request.Body)
160+ c.Assert(err, IsNil)
161+ expectedData, err := base64.StdEncoding.DecodeString(VHD_FOOTER)
162+ c.Assert(err, IsNil)
163+ c.Check(data, DeepEquals, expectedData)
164+ expected_range := fmt.Sprintf("bytes=%d-%d", VHD_SIZE-513, VHD_SIZE-1)
165+ c.Check(
166+ transport.Exchanges[1].Request.Header.Get("x-ms-range"),
167+ Equals, expected_range)
168+
169+ // Check the PutPage for the data exchange.
170+ data, err = ioutil.ReadAll(transport.Exchanges[2].Request.Body)
171+ c.Assert(err, IsNil)
172+ c.Check(data, DeepEquals, randomData)
173+
174+ c.Check(
175+ transport.Exchanges[2].Request.Header.Get("x-ms-range"),
176+ Equals, "bytes=0-511")
177+}
178+
179+func (s *testCreateInstanceDataVHD) TestSizeConstraints(c *C) {
180+ var err error
181+ context := makeStorageContext(&TestTransport{})
182+
183+ err = context.CreateInstanceDataVHD(&CreateVHDRequest{Size: 10})
184+ c.Check(err, ErrorMatches, "Size must be a multiple of 512")
185+
186+ err = context.CreateInstanceDataVHD(&CreateVHDRequest{
187+ Size: VHD_SIZE})
188+ errString := fmt.Sprintf("Size cannot be bigger than %d", VHD_SIZE-512)
189+ c.Check(err, ErrorMatches, errString)
190+}
191
192=== modified file 'vhd_footer.go'
193--- vhd_footer.go 2013-06-21 03:39:08 +0000
194+++ vhd_footer.go 2013-06-25 01:21:23 +0000
195@@ -20,6 +20,7 @@
196 // example), the easiest way is to use VirtualBox to define a new one, and
197 // then do 'tail -c 512 | base64' on that file.
198
199+const VHD_SIZE = 20972032 // This is 20Mib + 512 bytes
200 const VHD_FOOTER = `
201 Y29uZWN0aXgAAAACAAEAAP//////////GVKuuHZib3gABAACV2kyawAAAAABQAAAAAAAAAFAAAAC
202 WgQRAAAAAv//5y4OEjVapHY7QpuodZNf77j6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Subscribers

People subscribed via source and target branches

to all changes: