Skip to content

Commit 01e5e8c

Browse files
author
Foivos Filippopoulos
committed
Initial code commit, functions to create/delete snapshots
1 parent 6a2e6b7 commit 01e5e8c

File tree

4 files changed

+462
-0
lines changed

4 files changed

+462
-0
lines changed

main.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"os"
6+
"strings"
7+
8+
"github.com/utilitywarehouse/gcp-disk-snapshotter/models"
9+
"github.com/utilitywarehouse/gcp-disk-snapshotter/snapshot"
10+
"github.com/utilitywarehouse/gcp-disk-snapshotter/watch"
11+
)
12+
13+
var (
14+
// flags
15+
flagProject = flag.String("project", "", "(Required) GCP Project to use")
16+
flagZones = flag.String("zones", "", "(Required) Comma separated list of zones where projects disks may live")
17+
flagLabels = flag.String("labels", "", "(Required) Comma separated list of disk labels in format <name>:<value>")
18+
flagSnapPrefix = flag.String("snap_prefix", "", "Prefix for created snapshots")
19+
flagRetentionHours = flag.Int("retention_hours", 720, "Retention Duration in hours. Defaults to 720h = 1 month")
20+
flagIntervalSeconds = flag.Int("interval_secs", 43200, "Interval between snapshots in seconds. Defaults to 43200s = 12h")
21+
)
22+
23+
func usage() {
24+
flag.Usage()
25+
os.Exit(2)
26+
}
27+
28+
func main() {
29+
30+
// Flag Parsing
31+
flag.Parse()
32+
33+
if *flagProject == "" {
34+
usage()
35+
}
36+
project := *flagProject
37+
38+
if *flagZones == "" {
39+
usage()
40+
}
41+
zones := strings.Split(*flagZones, ",")
42+
43+
if *flagLabels == "" {
44+
usage()
45+
}
46+
47+
labels := &models.LabelList{}
48+
49+
for _, label := range strings.Split(*flagLabels, ",") {
50+
51+
l := strings.Split(label, ":")
52+
if len(l) < 2 {
53+
usage()
54+
}
55+
56+
labels.AddLabel(l[0], l[1])
57+
}
58+
59+
snapPrefix := *flagSnapPrefix
60+
retentionHours := *flagRetentionHours
61+
intervalSecs := *flagIntervalSeconds
62+
63+
// Create a snapshotter
64+
gsc := snapshot.CreateGCPSnapClient(project, snapPrefix, zones, *labels)
65+
66+
// Start watching
67+
watch.Watch(gsc, retentionHours, intervalSecs)
68+
69+
}

models/labels.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package models
2+
3+
type Label struct {
4+
Name string
5+
Value string
6+
}
7+
8+
type LabelList struct {
9+
Items []Label
10+
}
11+
12+
type LabelListInterface interface {
13+
AddLabel(name, value string)
14+
}
15+
16+
func (ll *LabelList) AddLabel(name, value string) {
17+
label := &Label{
18+
Name: name,
19+
Value: value,
20+
}
21+
ll.Items = append(ll.Items, *label)
22+
}

snapshot/client.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package snapshot
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"strings"
9+
"time"
10+
11+
"github.com/pkg/errors"
12+
"golang.org/x/oauth2/google"
13+
compute "google.golang.org/api/compute/v1"
14+
15+
"github.com/utilitywarehouse/gcp-disk-snapshotter/models"
16+
)
17+
18+
const (
19+
SnapshotterLabel string = "gcp_disk_snapshotter"
20+
SnapshotterLabelValue string = "true"
21+
)
22+
23+
var googleClient *http.Client
24+
25+
type GCPSnapClient struct {
26+
Project string
27+
Zones []string
28+
Labels models.LabelList
29+
SnapPrefix string
30+
ComputeService compute.Service
31+
}
32+
33+
type GCPSnapClientInterface interface {
34+
GetDiskList() ([]compute.Disk, error)
35+
ListSnapshots(diskSelfLink string) ([]compute.Snapshot, error)
36+
ListClientCreatedSnapshots(diskSelfLink string) ([]compute.Snapshot, error)
37+
CreateSnapshot() (string, error)
38+
DeleteSnapshot(snapName string) (string, error)
39+
GetZonalOperationStatus(operation, zone string) (string, error)
40+
GetGlobalOperationStatus(operation string) (string, error)
41+
}
42+
43+
// Basic Init function for the snapshotter
44+
func CreateGCPSnapClient(project, snapPrefix string, zones []string, labels models.LabelList) *GCPSnapClient {
45+
46+
ctx := context.Background()
47+
googleClient, err := google.DefaultClient(ctx, compute.ComputeScope)
48+
if err != nil {
49+
log.Fatal(err)
50+
}
51+
52+
computeService, err := compute.New(googleClient)
53+
if err != nil {
54+
log.Fatal(err)
55+
}
56+
57+
return &GCPSnapClient{
58+
Project: project,
59+
Zones: zones,
60+
Labels: labels,
61+
SnapPrefix: snapPrefix,
62+
ComputeService: *computeService,
63+
}
64+
}
65+
66+
// Helper function
67+
// In case of a gcp link it returns the target (final part after /)
68+
func formatLinkString(in string) string {
69+
70+
if strings.ContainsAny(in, "/") {
71+
elems := strings.Split(in, "/")
72+
return elems[len(elems)-1]
73+
}
74+
return in
75+
}
76+
77+
// GetDiskList: Returns a list of disks that contain one of the given labels
78+
func (gsc *GCPSnapClient) GetDiskList() ([]compute.Disk, error) {
79+
80+
disks := []compute.Disk{}
81+
82+
for _, zone := range gsc.Zones {
83+
resp, err := gsc.ComputeService.Disks.List(gsc.Project, zone).Do()
84+
if err != nil {
85+
return disks, errors.Wrap(err, "error listing disks")
86+
}
87+
88+
for _, disk := range resp.Items {
89+
for _, label := range gsc.Labels.Items {
90+
if val, ok := disk.Labels[label.Name]; ok {
91+
if label.Value == val {
92+
disks = append(disks, *disk)
93+
break
94+
}
95+
}
96+
}
97+
}
98+
}
99+
100+
return disks, nil
101+
}
102+
103+
// ListSnapshots: Lists Snapshots for a given disk
104+
func (gsc *GCPSnapClient) ListSnapshots(diskSelfLink string) ([]compute.Snapshot, error) {
105+
106+
snapshots := []compute.Snapshot{}
107+
108+
resp, err := gsc.ComputeService.Snapshots.List(gsc.Project).Do()
109+
if err != nil {
110+
return snapshots, errors.Wrap(err, "error requesting snapshots list:")
111+
}
112+
for _, snap := range resp.Items {
113+
// If not created by the snapshotter just ignore
114+
if val, ok := snap.Labels[SnapshotterLabel]; ok {
115+
if val != SnapshotterLabelValue {
116+
continue
117+
}
118+
} else {
119+
continue
120+
}
121+
122+
// If it's a snapshot of the input disk add to the list
123+
if snap.SourceDisk == diskSelfLink {
124+
snapshots = append(snapshots, *snap)
125+
}
126+
}
127+
return snapshots, nil
128+
}
129+
130+
// ListClientCreatedSnapshots: Lists snapshots for a given disk that were create by the client,
131+
// meaning that they have the SnapshotterLabel
132+
func (gsc *GCPSnapClient) ListClientCreatedSnapshots(diskSelfLink string) ([]compute.Snapshot, error) {
133+
134+
snaps, err := gsc.ListSnapshots(diskSelfLink)
135+
if err != nil {
136+
return []compute.Snapshot{}, err
137+
}
138+
139+
res := []compute.Snapshot{}
140+
for _, snap := range snaps {
141+
if val, ok := snap.Labels[SnapshotterLabel]; ok {
142+
if SnapshotterLabelValue == val {
143+
res = append(res, snap)
144+
continue
145+
}
146+
}
147+
}
148+
149+
return res, nil
150+
}
151+
152+
// CreateSnapshot: Gets a disk name and a zone, issues a create snapshot command to api
153+
// and returns a link to the create snapshot operation
154+
func (gsc *GCPSnapClient) CreateSnapshot(diskName, zone string) (string, error) {
155+
156+
// format zone if link
157+
zn := formatLinkString(zone)
158+
159+
// lowercase letters, numeric characters, underscores and dashes, at most 63 characters long
160+
snapLabels := map[string]string{
161+
SnapshotterLabel: SnapshotterLabelValue,
162+
}
163+
164+
// Name must match regex '(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)'
165+
snapshot := &compute.Snapshot{
166+
Description: fmt.Sprintf("Snapshot of %s", diskName),
167+
Name: fmt.Sprintf("%s%s-snapshot-%s", gsc.SnapPrefix, diskName, time.Now().Format("20060102150405")),
168+
Labels: snapLabels,
169+
}
170+
171+
resp, err := gsc.ComputeService.Disks.CreateSnapshot(gsc.Project, zn, diskName, snapshot).Do()
172+
if err != nil {
173+
return "", errors.Wrap(err, "error taking disk snapshot:")
174+
}
175+
176+
return resp.SelfLink, nil
177+
}
178+
179+
// DeleteSnapshot: Gets a snapshot name and issues a delete. Returns a link to the delete operation
180+
func (gsc *GCPSnapClient) DeleteSnapshot(snapName string) (string, error) {
181+
182+
resp, err := gsc.ComputeService.Snapshots.Delete(gsc.Project, snapName).Do()
183+
if err != nil {
184+
return "", errors.Wrap(err, "error deleting snapshot:")
185+
}
186+
187+
return resp.SelfLink, nil
188+
}
189+
190+
func (gsc *GCPSnapClient) GetZonalOperationStatus(operation, zone string) (string, error) {
191+
192+
// Format in case of link
193+
operation = formatLinkString(operation)
194+
zone = formatLinkString(zone)
195+
196+
op, err := gsc.ComputeService.ZoneOperations.Get(gsc.Project, zone, operation).Do()
197+
if err != nil {
198+
return "", errors.Wrap(err, "error getting zonal operation:")
199+
}
200+
201+
// Get status (Possible values: "DONE", "PENDING", "RUNNING") and errors
202+
status := op.Status
203+
if op.Error != nil {
204+
var err_msgs []string
205+
for _, err := range op.Error.Errors {
206+
err_msgs = append(err_msgs, err.Message)
207+
}
208+
return status, errors.New(strings.Join(err_msgs, ","))
209+
}
210+
return status, nil
211+
}
212+
213+
func (gsc *GCPSnapClient) GetGlobalOperationStatus(operation string) (string, error) {
214+
215+
// Format in case of link
216+
operation = formatLinkString(operation)
217+
218+
op, err := gsc.ComputeService.GlobalOperations.Get(gsc.Project, operation).Do()
219+
if err != nil {
220+
return "", errors.Wrap(err, "error getting global operation:")
221+
}
222+
223+
// Get status (Possible values: "DONE", "PENDING", "RUNNING") and errors
224+
status := op.Status
225+
if op.Error != nil {
226+
var err_msgs []string
227+
for _, err := range op.Error.Errors {
228+
err_msgs = append(err_msgs, err.Message)
229+
}
230+
return status, errors.New(strings.Join(err_msgs, ","))
231+
}
232+
return status, nil
233+
}

0 commit comments

Comments
 (0)