Merge lp:~allenap/gwacl/destroy-everything into lp:gwacl

Proposed by Gavin Panella
Status: Needs review
Proposed branch: lp:~allenap/gwacl/destroy-everything
Merge into: lp:gwacl
Diff against target: 471 lines (+382/-11)
5 files modified
example/destroyer/run.go (+113/-0)
management_base.go (+31/-0)
management_base_test.go (+104/-0)
xmlobjects.go (+36/-1)
xmlobjects_test.go (+98/-10)
To merge this branch: bzr merge lp:~allenap/gwacl/destroy-everything
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+177420@code.launchpad.net

Commit message

Script to destroy most things belonging to a subscription.

Description of the change

I wrote this because I had loads of artifacts in my Azure account that would take me a day to remove, which is more time than it has taken to implement this script and run it.

Along the way I had to implement ListDisks() and ListStorageAccounts(). It also uncovered an error in the XML that's generated in TestStorageServicesUnmarshal(), which is an argument for using the example XML basically as-is from the Azure docs. This actually seems to be what we're doing these days, but it's interesting to note anyway.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

This looks helpful — although please say very clearly at the top of the destroyer program what it does! Otherwise people might expect it to prompt for the thing they want to delete, or something.

The literal XML in tests leads to funny situations, like "true|false" as a Boolean field, but it works for me.

One thing I'm not a fan of is the happy-path-only tests with names like TestFunction. When you push yourself there's usually a corner case you can find that could gainfully be tested. It's a useful habit not just for improving test coverage, but for honing testability skills as well — it trains you to recognize unnecessarily complex code, code that's hard to test exhaustively, unnecessary special cases, and forgotten corner cases.

Revision history for this message
Jeroen T. Vermeulen (jtv) :
review: Approve

Unmerged revisions

211. By Gavin Panella

Remove virtual networks too.

210. By Gavin Panella

Example script to destroy (almost) everything.

209. By Gavin Panella

Whitespace.

208. By Gavin Panella

New method ListDisks().

207. By Gavin Panella

New struct for Disks.

206. By Gavin Panella

New method, ListStorageAccounts.

205. By Gavin Panella

Invalid StorageServices template for test.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'example/destroyer'
2=== added file 'example/destroyer/run.go'
3--- example/destroyer/run.go 1970-01-01 00:00:00 +0000
4+++ example/destroyer/run.go 2013-07-29 15:50:39 +0000
5@@ -0,0 +1,113 @@
6+// Copyright 2013 Canonical Ltd. This software is licensed under the
7+// GNU Lesser General Public License version 3 (see the file COPYING).
8+
9+/*
10+This is an example on how the Azure Go library can be used to interact with
11+the Windows Azure Service.
12+Note that this is a provided only as an example and that real code should
13+probably do something more sensible with errors than ignoring them or panicking.
14+*/
15+package main
16+
17+import (
18+ "flag"
19+ "fmt"
20+ "launchpad.net/gwacl"
21+ "os"
22+)
23+
24+var certFile string
25+var subscriptionID string
26+
27+func getParams() error {
28+ flag.StringVar(&certFile, "cert", "", "Name of your management certificate file (in PEM format).")
29+ flag.StringVar(&subscriptionID, "subscriptionid", "", "Your Azure subscription ID.")
30+ flag.Parse()
31+ if certFile == "" {
32+ return fmt.Errorf("No .pem certificate specified. Use the -cert option.")
33+ }
34+ if subscriptionID == "" {
35+ return fmt.Errorf("No subscription ID specified. Use the -subscriptionid option.")
36+ }
37+ return nil
38+}
39+
40+func checkError(err error) {
41+ if err != nil {
42+ panic(err)
43+ }
44+}
45+
46+func main() {
47+ err := getParams()
48+ if err != nil {
49+ fmt.Println(err)
50+ os.Exit(1)
51+ }
52+
53+ api, err := gwacl.NewManagementAPI(subscriptionID, certFile)
54+ checkError(err)
55+
56+ DestroyEverything(api)
57+}
58+
59+func DestroyEverything(api *gwacl.ManagementAPI) {
60+ hostedServices, err := api.ListHostedServices()
61+ checkError(err)
62+
63+ for _, hostedService := range hostedServices {
64+ err := api.DestroyHostedService(
65+ &gwacl.DestroyHostedServiceRequest{hostedService.ServiceName})
66+ checkError(err)
67+ }
68+
69+ disks, err := api.ListDisks()
70+ checkError(err)
71+
72+ // Delete all disks and their storage.
73+ for _, disk := range disks {
74+ err := api.DeleteDisk(&gwacl.DeleteDiskRequest{
75+ DiskName: disk.Name,
76+ DeleteBlob: true,
77+ })
78+ checkError(err)
79+ }
80+
81+ storageAccounts, err := api.ListStorageAccounts()
82+ checkError(err)
83+
84+ for _, storageAccount := range storageAccounts {
85+ storageKeys, err := api.GetStorageAccountKeys(storageAccount.ServiceName)
86+ checkError(err)
87+
88+ storageContext := &gwacl.StorageContext{
89+ Account: storageAccount.ServiceName,
90+ Key: storageKeys.Primary,
91+ }
92+
93+ storageContainers, err := storageContext.ListAllContainers()
94+ checkError(err)
95+
96+ // Delete all blobs in all containers, and the containers themselves.
97+ for _, storageContainer := range storageContainers.Containers {
98+ err := storageContext.DeleteAllBlobs(
99+ &gwacl.DeleteAllBlobsRequest{storageContainer.Name})
100+ checkError(err)
101+ err = storageContext.DeleteContainer(storageContainer.Name)
102+ checkError(err)
103+ }
104+
105+ // Delete the storage account.
106+ err = api.DeleteStorageAccount(storageAccount.ServiceName)
107+ checkError(err)
108+ }
109+
110+ // Remove virtual networks.
111+ networkConfig, err := api.GetNetworkConfiguration()
112+ checkError(err)
113+ if networkConfig != nil && networkConfig.VirtualNetworkSites != nil {
114+ networkConfig.VirtualNetworkSites = &[]gwacl.VirtualNetworkSite{}
115+ err := api.SetNetworkConfiguration(networkConfig)
116+ checkError(err)
117+ }
118+}
119
120=== modified file 'management_base.go'
121--- management_base.go 2013-07-25 22:02:41 +0000
122+++ management_base.go 2013-07-29 15:50:39 +0000
123@@ -274,6 +274,23 @@
124 return &deployment, nil
125 }
126
127+// ListStorageAccount lists all storage accounts. This is called a storage
128+// service in the Azure API, but nomenclature seems to have changed.
129+// See http://msdn.microsoft.com/en-us/library/windowsazure/ee460787.aspx
130+func (api *ManagementAPI) ListStorageAccounts() ([]StorageService, error) {
131+ uri := "services/storageservices"
132+ response, err := api.session.get(uri, "2012-03-01")
133+ if err != nil {
134+ return nil, err
135+ }
136+ services := &StorageServices{}
137+ err = services.Deserialize(response.Body)
138+ if err != nil {
139+ return nil, err
140+ }
141+ return services.StorageServices, nil
142+}
143+
144 // AddStorageAccount starts the creation of a storage account. This is
145 // called a storage service in the Azure API, but nomenclature seems to
146 // have changed.
147@@ -324,6 +341,20 @@
148 return &keys, nil
149 }
150
151+// ListDisk lists all disks.
152+func (api *ManagementAPI) ListDisks() ([]Disk, error) {
153+ res, err := api.session.get("services/disks", "2012-03-01")
154+ if err != nil {
155+ return nil, err
156+ }
157+ disks := &Disks{}
158+ err = disks.Deserialize(res.Body)
159+ if err != nil {
160+ return nil, err
161+ }
162+ return disks.Disks, nil
163+}
164+
165 type DeleteDiskRequest struct {
166 DiskName string // Name of the disk to delete.
167 DeleteBlob bool // Whether to delete the associated blob storage.
168
169=== modified file 'management_base_test.go'
170--- management_base_test.go 2013-07-25 22:02:41 +0000
171+++ management_base_test.go 2013-07-29 15:50:39 +0000
172@@ -802,6 +802,80 @@
173 c.Check(deployment.Name, Equals, deploymentName)
174 }
175
176+func (suite *managementBaseAPISuite) TestListStorageAccounts(c *C) {
177+ body := dedent.Dedent(`
178+ <?xml version="1.0" encoding="utf-8"?>
179+ <StorageServices xmlns="http://schemas.microsoft.com/windowsazure">
180+ <StorageService>
181+ <Url>storage-service-address</Url>
182+ <ServiceName>storage-service-name</ServiceName>
183+ <StorageServiceProperties>
184+ <Description>description</Description>
185+ <AffinityGroup>affinity-group</AffinityGroup>
186+ <Label>base64-encoded-label</Label>
187+ <Status>status</Status>
188+ <Endpoints>
189+ <Endpoint>storage-service-blob-endpoint</Endpoint>
190+ <Endpoint>storage-service-queue-endpoint</Endpoint>
191+ <Endpoint>storage-service-table-endpoint</Endpoint>
192+ </Endpoints>
193+ <GeoReplicationEnabled>true|false</GeoReplicationEnabled>
194+ <GeoPrimaryRegion>primary-region</GeoPrimaryRegion>
195+ <StatusOfPrimary>Available|Unavailable</StatusOfPrimary>
196+ <LastGeoFailoverTime>DateTime</LastGeoFailoverTime>
197+ <GeoSecondaryRegion>secondary-region</GeoSecondaryRegion>
198+ <StatusOfSecondary>Available|Unavailable</StatusOfSecondary>
199+ <CreationTime>time-of-creation</CreationTime>
200+ </StorageServiceProperties>
201+ <ExtendedProperties>
202+ <ExtendedProperty>
203+ <Name>property-name</Name>
204+ <Value>property-value</Value>
205+ </ExtendedProperty>
206+ </ExtendedProperties>
207+ </StorageService>
208+ </StorageServices>
209+ `)
210+ rigFixedResponseDispatcher(&x509Response{
211+ StatusCode: http.StatusOK,
212+ Body: []byte(body),
213+ })
214+ api := makeAPI(c)
215+ recordedRequests := make([]*X509Request, 0)
216+ rigRecordingDispatcher(&recordedRequests)
217+
218+ accounts, err := api.ListStorageAccounts()
219+
220+ c.Assert(err, IsNil)
221+ c.Assert(recordedRequests, HasLen, 1)
222+ expectedURL := api.session.composeURL("services/storageservices")
223+ c.Assert(recordedRequests[0].URL, Equals, expectedURL)
224+ c.Check(accounts, HasLen, 1)
225+ c.Check(accounts[0], DeepEquals, StorageService{
226+ URL: "storage-service-address",
227+ ServiceName: "storage-service-name",
228+ Description: "description",
229+ AffinityGroup: "affinity-group",
230+ Label: "base64-encoded-label",
231+ Status: "status",
232+ Endpoints: []string{
233+ "storage-service-blob-endpoint",
234+ "storage-service-queue-endpoint",
235+ "storage-service-table-endpoint",
236+ },
237+ GeoReplicationEnabled: "true|false",
238+ GeoPrimaryRegion: "primary-region",
239+ StatusOfPrimary: "Available|Unavailable",
240+ LastGeoFailoverTime: "DateTime",
241+ GeoSecondaryRegion: "secondary-region",
242+ StatusOfSecondary: "Available|Unavailable",
243+ CreationTime: "time-of-creation",
244+ ExtendedProperties: []ExtendedProperty{
245+ {"property-name", "property-value"},
246+ },
247+ })
248+}
249+
250 func (suite *managementBaseAPISuite) TestAddStorageAccount(c *C) {
251 api := makeAPI(c)
252 header := http.Header{}
253@@ -872,6 +946,36 @@
254 c.Check(keys.URL, Equals, url)
255 }
256
257+func (suite *managementBaseAPISuite) TestListDisks(c *C) {
258+ // This is a minimal response body; Disks XML docs are tested elsewhere.
259+ responseBody := `
260+ <Disks xmlns="http://schemas.microsoft.com/windowsazure">
261+ <Disk>
262+ <Name>disk-name-1</Name>
263+ </Disk>
264+ <Disk>
265+ <Name>disk-name-2</Name>
266+ </Disk>
267+ </Disks>`
268+ rigFixedResponseDispatcher(&x509Response{
269+ StatusCode: http.StatusOK,
270+ Body: []byte(responseBody),
271+ })
272+ recordedRequests := make([]*X509Request, 0)
273+ rigRecordingDispatcher(&recordedRequests)
274+
275+ disks, err := makeAPI(c).ListDisks()
276+
277+ c.Assert(err, IsNil)
278+ c.Assert(recordedRequests, HasLen, 1)
279+ c.Check(recordedRequests[0].Method, Equals, "GET")
280+ c.Check(recordedRequests[0].URL, Equals, AZURE_URL+"subscriptionId/services/disks")
281+ c.Check(disks, HasLen, 2)
282+ c.Check(disks, DeepEquals, []Disk{
283+ {Name: "disk-name-1"}, {Name: "disk-name-2"},
284+ })
285+}
286+
287 func assertDeleteDiskRequest(c *C, api *ManagementAPI, diskName string, httpRequest *X509Request) {
288 expectedURL := fmt.Sprintf("%s%s/services/disks/%s", AZURE_URL,
289 api.session.subscriptionId, diskName)
290
291=== modified file 'xmlobjects.go'
292--- xmlobjects.go 2013-07-25 22:02:41 +0000
293+++ xmlobjects.go 2013-07-29 15:50:39 +0000
294@@ -251,6 +251,40 @@
295 }
296
297 //
298+// Disk, as returned by List Disks
299+//
300+
301+type AttachedTo struct {
302+ HostedServiceName string `xml:"HostedServiceName"`
303+ DeploymentName string `xml:"DeploymentName"`
304+ RoleName string `xml:"RoleName"`
305+}
306+
307+type Disk struct {
308+ AffinityGroup string `xml:"AffinityGroup,omitempty"`
309+ AttachedTo *AttachedTo `xml:"AttachedTo,omitempty"`
310+ OS string `xml:"OS,omitempty"`
311+ Location string `xml:"Location,omitempty`
312+ LogicalSizeInGB string `xml:"LogicalSizeInGB,omitempty"`
313+ MediaLink string `xml:"MediaLink,omitempty"`
314+ Name string `xml:"Name"`
315+ SourceImageName string `xml:"SourceImageName,omitempty"`
316+}
317+
318+type Disks struct {
319+ XMLNS string `xml:"xmlns,attr"`
320+ Disks []Disk `xml:"Disk"`
321+}
322+
323+func (disks *Disks) Serialize() (string, error) {
324+ return toxml(disks)
325+}
326+
327+func (disks *Disks) Deserialize(data []byte) error {
328+ return xml.Unmarshal(data, disks)
329+}
330+
331+//
332 // DataVirtualHardDisk
333 //
334
335@@ -764,7 +798,8 @@
336 LastGeoFailoverTime string `xml:"StorageServiceProperties>LastGeoFailoverTime"`
337 GeoSecondaryRegion string `xml:"StorageServiceProperties>GeoSecondaryRegion"`
338 StatusOfSecondary string `xml:"StorageServiceProperties>StatusOfSecondary"`
339- ExtendedProperties []ExtendedProperty `xml:"StorageServiceProperties>ExtendedProperties>ExtendedProperty"`
340+ CreationTime string `xml:"StorageServiceProperties>CreationTime"`
341+ ExtendedProperties []ExtendedProperty `xml:"ExtendedProperties>ExtendedProperty"`
342
343 // TODO: Add accessors for non-string data encoded as strings.
344 }
345
346=== modified file 'xmlobjects_test.go'
347--- xmlobjects_test.go 2013-07-25 22:02:41 +0000
348+++ xmlobjects_test.go 2013-07-29 15:50:39 +0000
349@@ -67,6 +67,94 @@
350 c.Check(strings.TrimSpace(xml), Equals, strings.TrimSpace(expected))
351 }
352
353+func (suite *xmlSuite) TestDisksSerialize(c *C) {
354+ disks := &Disks{
355+ XMLNS: XMLNS,
356+ Disks: []Disk{
357+ {
358+ AffinityGroup: "affinity-group",
359+ AttachedTo: &AttachedTo{
360+ HostedServiceName: "hosted-service-name",
361+ DeploymentName: "deployment-name",
362+ RoleName: "role-name",
363+ },
364+ OS: "os",
365+ Location: "location",
366+ LogicalSizeInGB: "logical-size-in-gb",
367+ MediaLink: "media-link",
368+ Name: "name",
369+ SourceImageName: "source-image-name",
370+ },
371+ },
372+ }
373+ observed, err := disks.Serialize()
374+ c.Assert(err, IsNil)
375+ expected := strings.TrimSpace(dedent.Dedent(`
376+ <Disks xmlns="http://schemas.microsoft.com/windowsazure">
377+ <Disk>
378+ <AffinityGroup>affinity-group</AffinityGroup>
379+ <AttachedTo>
380+ <HostedServiceName>hosted-service-name</HostedServiceName>
381+ <DeploymentName>deployment-name</DeploymentName>
382+ <RoleName>role-name</RoleName>
383+ </AttachedTo>
384+ <OS>os</OS>
385+ <Location>location</Location>
386+ <LogicalSizeInGB>logical-size-in-gb</LogicalSizeInGB>
387+ <MediaLink>media-link</MediaLink>
388+ <Name>name</Name>
389+ <SourceImageName>source-image-name</SourceImageName>
390+ </Disk>
391+ </Disks>`))
392+ c.Check(observed, Equals, expected)
393+
394+}
395+
396+func (suite *xmlSuite) TestDisksDeserialize(c *C) {
397+ // From http://msdn.microsoft.com/en-us/library/windowsazure/jj157176.aspx
398+ input := `
399+ <Disks xmlns="http://schemas.microsoft.com/windowsazure">
400+ <Disk>
401+ <AffinityGroup>affinity-group-name</AffinityGroup>
402+ <AttachedTo>
403+ <HostedServiceName>hostedService-name</HostedServiceName>
404+ <DeploymentName>deployment-name</DeploymentName>
405+ <RoleName>role-name</RoleName>
406+ </AttachedTo>
407+ <OS>Linux|Windows</OS>
408+ <Location>geo-location-of-the-stored-disk</Location>
409+ <LogicalSizeInGB>size-of-the-disk</LogicalSizeInGB>
410+ <MediaLink>uri-of-the-containing-blob</MediaLink>
411+ <Name>disk-name</Name>
412+ <SourceImageName>source-image-name</SourceImageName>
413+ </Disk>
414+ ...
415+ </Disks>`
416+ expected := &Disks{
417+ XMLNS: XMLNS,
418+ Disks: []Disk{
419+ {
420+ AffinityGroup: "affinity-group-name",
421+ AttachedTo: &AttachedTo{
422+ HostedServiceName: "hostedService-name",
423+ DeploymentName: "deployment-name",
424+ RoleName: "role-name",
425+ },
426+ OS: "Linux|Windows",
427+ Location: "geo-location-of-the-stored-disk",
428+ LogicalSizeInGB: "size-of-the-disk",
429+ MediaLink: "uri-of-the-containing-blob",
430+ Name: "disk-name",
431+ SourceImageName: "source-image-name",
432+ },
433+ },
434+ }
435+ observed := &Disks{}
436+ err := observed.Deserialize([]byte(input))
437+ c.Assert(err, IsNil)
438+ c.Check(observed, DeepEquals, expected)
439+}
440+
441 func (suite *xmlSuite) TestOSVirtualHardDisk(c *C) {
442 disk := makeOSVirtualHardDisk()
443
444@@ -1113,17 +1201,17 @@
445 <LastGeoFailoverTime>%s</LastGeoFailoverTime>
446 <GeoSecondaryRegion>%s</GeoSecondaryRegion>
447 <StatusOfSecondary>%s</StatusOfSecondary>
448- <ExtendedProperties>
449- <ExtendedProperty>
450- <Name>%s</Name>
451- <Value>%s</Value>
452- </ExtendedProperty>
453- <ExtendedProperty>
454- <Name>%s</Name>
455- <Value>%s</Value>
456- </ExtendedProperty>
457- </ExtendedProperties>
458 </StorageServiceProperties>
459+ <ExtendedProperties>
460+ <ExtendedProperty>
461+ <Name>%s</Name>
462+ <Value>%s</Value>
463+ </ExtendedProperty>
464+ <ExtendedProperty>
465+ <Name>%s</Name>
466+ <Value>%s</Value>
467+ </ExtendedProperty>
468+ </ExtendedProperties>
469 </StorageService>
470 </StorageServices>`
471 url := MakeRandomString(10)

Subscribers

People subscribed via source and target branches

to all changes: