package google import ( "fmt" "io/ioutil" "net/url" "regexp" "strings" "time" "github.com/docker/machine/libmachine/log" raw "google.golang.org/api/compute/v1" "errors" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/googleapi" ) // ComputeUtil is used to wrap the raw GCE API code and store common parameters. type ComputeUtil struct { zone string instanceName string userName string project string diskTypeURL string address string preemptible bool useInternalIP bool service *raw.Service zoneURL string globalURL string SwarmMaster bool SwarmHost string } const ( apiURL = "https://www.googleapis.com/compute/v1/projects/" firewallRule = "docker-machines" port = "2376" firewallTargetTag = "docker-machine" dockerStartCommand = "sudo service docker start" dockerStopCommand = "sudo service docker stop" ) // NewComputeUtil creates and initializes a ComputeUtil. func newComputeUtil(driver *Driver) (*ComputeUtil, error) { client, err := google.DefaultClient(oauth2.NoContext, raw.ComputeScope) if err != nil { return nil, err } service, err := raw.New(client) if err != nil { return nil, err } return &ComputeUtil{ zone: driver.Zone, instanceName: driver.MachineName, userName: driver.SSHUser, project: driver.Project, diskTypeURL: driver.DiskType, address: driver.Address, preemptible: driver.Preemptible, useInternalIP: driver.UseInternalIP, service: service, zoneURL: apiURL + driver.Project + "/zones/" + driver.Zone, globalURL: apiURL + driver.Project + "/global", SwarmMaster: driver.SwarmMaster, SwarmHost: driver.SwarmHost, }, nil } func (c *ComputeUtil) diskName() string { return c.instanceName + "-disk" } func (c *ComputeUtil) diskType() string { return apiURL + c.project + "/zones/" + c.zone + "/diskTypes/" + c.diskTypeURL } // disk returns the persistent disk attached to the vm. func (c *ComputeUtil) disk() (*raw.Disk, error) { return c.service.Disks.Get(c.project, c.zone, c.diskName()).Do() } // deleteDisk deletes the persistent disk. func (c *ComputeUtil) deleteDisk() error { log.Infof("Deleting disk.") op, err := c.service.Disks.Delete(c.project, c.zone, c.diskName()).Do() if err != nil { return err } log.Infof("Waiting for disk to delete.") return c.waitForRegionalOp(op.Name) } // staticAddress returns the external static IP address. func (c *ComputeUtil) staticAddress() (string, error) { // is the address a name? isName, err := regexp.MatchString("[a-z]([-a-z0-9]*[a-z0-9])?", c.address) if err != nil { return "", err } if !isName { return c.address, nil } // resolve the address by name externalAddress, err := c.service.Addresses.Get(c.project, c.region(), c.address).Do() if err != nil { return "", err } return externalAddress.Address, nil } func (c *ComputeUtil) region() string { return c.zone[:len(c.zone)-2] } func (c *ComputeUtil) firewallRule() (*raw.Firewall, error) { return c.service.Firewalls.Get(c.project, firewallRule).Do() } func missingOpenedPorts(rule *raw.Firewall, ports []string) []string { missing := []string{} opened := map[string]bool{} for _, allowed := range rule.Allowed { for _, allowedPort := range allowed.Ports { opened[allowedPort] = true } } for _, port := range ports { if !opened[port] { missing = append(missing, port) } } return missing } func (c *ComputeUtil) portsUsed() ([]string, error) { ports := []string{port} if c.SwarmMaster { u, err := url.Parse(c.SwarmHost) if err != nil { return nil, fmt.Errorf("error authorizing port for swarm: %s", err) } swarmPort := strings.Split(u.Host, ":")[1] ports = append(ports, swarmPort) } return ports, nil } func (c *ComputeUtil) createFirewallRule() error { log.Infof("Opening firewall ports.") create := false rule, _ := c.firewallRule() if rule == nil { create = true rule = &raw.Firewall{ Name: firewallRule, Allowed: []*raw.FirewallAllowed{}, SourceRanges: []string{"0.0.0.0/0"}, TargetTags: []string{firewallTargetTag}, } } portsUsed, err := c.portsUsed() if err != nil { return err } missingPorts := missingOpenedPorts(rule, portsUsed) if len(missingPorts) == 0 { return nil } rule.Allowed = append(rule.Allowed, &raw.FirewallAllowed{ IPProtocol: "tcp", Ports: missingPorts, }) var op *raw.Operation if create { op, err = c.service.Firewalls.Insert(c.project, rule).Do() } else { op, err = c.service.Firewalls.Update(c.project, firewallRule, rule).Do() } if err != nil { return err } return c.waitForGlobalOp(op.Name) } // instance retrieves the instance. func (c *ComputeUtil) instance() (*raw.Instance, error) { return c.service.Instances.Get(c.project, c.zone, c.instanceName).Do() } // createInstance creates a GCE VM instance. func (c *ComputeUtil) createInstance(d *Driver) error { log.Infof("Creating instance.") if err := c.createFirewallRule(); err != nil { return err } instance := &raw.Instance{ Name: c.instanceName, Description: "docker host vm", MachineType: c.zoneURL + "/machineTypes/" + d.MachineType, Disks: []*raw.AttachedDisk{ { Boot: true, AutoDelete: false, Type: "PERSISTENT", Mode: "READ_WRITE", }, }, NetworkInterfaces: []*raw.NetworkInterface{ { AccessConfigs: []*raw.AccessConfig{ {Type: "ONE_TO_ONE_NAT"}, }, Network: c.globalURL + "/networks/default", }, }, Tags: &raw.Tags{ Items: parseTags(d), }, ServiceAccounts: []*raw.ServiceAccount{ { Email: "default", Scopes: strings.Split(d.Scopes, ","), }, }, Scheduling: &raw.Scheduling{ Preemptible: c.preemptible, }, } if c.address != "" { staticAddress, err := c.staticAddress() if err != nil { return err } instance.NetworkInterfaces[0].AccessConfigs[0].NatIP = staticAddress } disk, err := c.disk() if disk == nil || err != nil { instance.Disks[0].InitializeParams = &raw.AttachedDiskInitializeParams{ DiskName: c.diskName(), SourceImage: d.MachineImage, // The maximum supported disk size is 1000GB, the cast should be fine. DiskSizeGb: int64(d.DiskSize), DiskType: c.diskType(), } } else { instance.Disks[0].Source = c.zoneURL + "/disks/" + c.instanceName + "-disk" } op, err := c.service.Instances.Insert(c.project, c.zone, instance).Do() if err != nil { return err } log.Infof("Waiting for Instance...") if err = c.waitForRegionalOp(op.Name); err != nil { return err } instance, err = c.instance() if err != nil { return err } // Update the SSH Key sshKey, err := ioutil.ReadFile(d.GetSSHKeyPath() + ".pub") if err != nil { return err } log.Infof("Uploading SSH Key") metaDataValue := fmt.Sprintf("%s:%s %s\n", c.userName, strings.TrimSpace(string(sshKey)), c.userName) op, err = c.service.Instances.SetMetadata(c.project, c.zone, c.instanceName, &raw.Metadata{ Fingerprint: instance.Metadata.Fingerprint, Items: []*raw.MetadataItems{ { Key: "sshKeys", Value: &metaDataValue, }, }, }).Do() if err != nil { return err } log.Infof("Waiting for SSH Key") return c.waitForRegionalOp(op.Name) } // parseTags computes the tags for the instance. func parseTags(d *Driver) []string { tags := []string{firewallTargetTag} if d.Tags != "" { tags = append(tags, strings.Split(d.Tags, ",")...) } return tags } // deleteInstance deletes the instance, leaving the persistent disk. func (c *ComputeUtil) deleteInstance() error { log.Infof("Deleting instance.") op, err := c.service.Instances.Delete(c.project, c.zone, c.instanceName).Do() if err != nil { return err } log.Infof("Waiting for instance to delete.") return c.waitForRegionalOp(op.Name) } // stopInstance stops the instance. func (c *ComputeUtil) stopInstance() error { log.Infof("Stopping instance.") op, err := c.service.Instances.Stop(c.project, c.zone, c.instanceName).Do() if err != nil { return err } log.Infof("Waiting for instance to stop.") return c.waitForRegionalOp(op.Name) } // startInstance starts the instance. func (c *ComputeUtil) startInstance() error { log.Infof("Starting instance.") op, err := c.service.Instances.Start(c.project, c.zone, c.instanceName).Do() if err != nil { return err } log.Infof("Waiting for instance to start.") return c.waitForRegionalOp(op.Name) } func (c *ComputeUtil) waitForOp(opGetter func() (*raw.Operation, error)) error { for { op, err := opGetter() if err != nil { return err } log.Debugf("operation %q status: %s", op.Name, op.Status) if op.Status == "DONE" { if op.Error != nil { return fmt.Errorf("Operation error: %v", *op.Error.Errors[0]) } break } time.Sleep(1 * time.Second) } return nil } // waitForOp waits for the operation to finish. func (c *ComputeUtil) waitForRegionalOp(name string) error { return c.waitForOp(func() (*raw.Operation, error) { return c.service.ZoneOperations.Get(c.project, c.zone, name).Do() }) } // waitForGlobalOp waits for the global operation to finish. func (c *ComputeUtil) waitForGlobalOp(name string) error { return c.waitForOp(func() (*raw.Operation, error) { return c.service.GlobalOperations.Get(c.project, name).Do() }) } // ip retrieves and returns the external IP address of the instance. func (c *ComputeUtil) ip() (string, error) { instance, err := c.service.Instances.Get(c.project, c.zone, c.instanceName).Do() if err != nil { return "", unwrapGoogleError(err) } nic := instance.NetworkInterfaces[0] if c.useInternalIP { return nic.NetworkIP, nil } return nic.AccessConfigs[0].NatIP, nil } func unwrapGoogleError(err error) error { if googleErr, ok := err.(*googleapi.Error); ok { return errors.New(googleErr.Message) } return err }