From 585d0f360766f7d956a25beb31c1bb271163f0cc Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 4 Jan 2020 00:02:30 +0000 Subject: [PATCH] Add ability to delete using ip address Added lookup functions to allow the use of the ip address instead of the id when deleting exit nodes Signed-off-by: Adam Johnson --- cmd/delete.go | 52 +++++++++----- pkg/provision/civo.go | 128 +++++++++++++++++++++++----------- pkg/provision/digitalocean.go | 70 ++++++++++++++++++- pkg/provision/ec2.go | 75 +++++++++++++++++++- pkg/provision/gce.go | 111 +++++++++++++++++++++++------ pkg/provision/packet.go | 56 ++++++++++++++- pkg/provision/provision.go | 21 +++++- pkg/provision/scaleway.go | 78 ++++++++++++++++++--- 8 files changed, 489 insertions(+), 102 deletions(-) diff --git a/cmd/delete.go b/cmd/delete.go index 20df1b17..3ec98250 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -6,6 +6,7 @@ package cmd import ( "fmt" + "github.com/inlets/inletsctl/pkg/provision" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -14,15 +15,19 @@ func init() { inletsCmd.AddCommand(deleteCmd) deleteCmd.Flags().StringP("provider", "p", "digitalocean", "The cloud provider - digitalocean, gce, ec2, packet, scaleway, or civo") deleteCmd.Flags().StringP("region", "r", "lon1", "The region for your cloud provider") + deleteCmd.Flags().StringP("zone", "z", "us-central1-a", "The zone for the exit node (Google Compute Engine)") deleteCmd.Flags().StringP("access-token", "a", "", "The access token for your cloud") deleteCmd.Flags().StringP("access-token-file", "f", "", "Read this file for the access token for your cloud") deleteCmd.Flags().StringP("id", "i", "", "Host ID") + deleteCmd.Flags().String("ip", "", "Host IP") deleteCmd.Flags().String("secret-key", "", "The access token for your cloud (Scaleway, EC2)") deleteCmd.Flags().String("secret-key-file", "", "Read this file for the access token for your cloud (Scaleway, EC2)") deleteCmd.Flags().String("organisation-id", "", "Organisation ID (Scaleway)") + deleteCmd.Flags().String("project-id", "", "Project ID (Packet.com, Google Compute Engine)") + } // deleteCmd represents the client sub command @@ -49,7 +54,7 @@ func runDelete(cmd *cobra.Command, _ []string) error { var region string if cmd.Flags().Changed("region") { - if regionVal, err := cmd.Flags().GetString("region"); len(regionVal) > 0 { + if regionVal, err := cmd.Flags().GetString("region"); isSet(regionVal) { if err != nil { return errors.Wrap(err, "failed to get 'region' value.") } @@ -62,19 +67,6 @@ func runDelete(cmd *cobra.Command, _ []string) error { region = "eu-west-1" } - inletsToken, err := cmd.Flags().GetString("inlets-token") - if err != nil { - return errors.Wrap(err, "failed to get 'inlets-token' value.") - } - if len(inletsToken) == 0 { - var passwordErr error - inletsToken, passwordErr = generateAuth() - - if passwordErr != nil { - return passwordErr - } - } - accessToken, err := getFileOrString(cmd.Flags(), "access-token-file", "access-token", true) if err != nil { return err @@ -104,17 +96,41 @@ func runDelete(cmd *cobra.Command, _ []string) error { } hostID, _ := cmd.Flags().GetString("id") + hostIP, _ := cmd.Flags().GetString("ip") + projectID, _ := cmd.Flags().GetString("project-id") + zone, _ := cmd.Flags().GetString("zone") + + if isNotSet(hostID) && isNotSet(hostIP) { + return fmt.Errorf("give a valid --id or --ip for your host") + } + + if provider == "gce" && isSet(hostIP) { + if isNotSet(projectID) { + return fmt.Errorf("--ip requires --project-id to be set for provider") + } + } - if len(hostID) == 0 { - return fmt.Errorf("give a valid --id for your host") + deleteRequest := provision.HostDeleteRequest{ + ID: hostID, + IP: hostIP, + ProjectID: projectID, + Zone: zone, } - fmt.Printf("Deleting host: %s from %s\n", hostID, provider) + fmt.Printf("Deleting host: %s%s from %s\n", hostID, hostIP, provider) - err = provisioner.Delete(hostID) + err = provisioner.Delete(deleteRequest) if err != nil { return err } return err } + +func isNotSet(s string) bool { + return len(s) == 0 +} + +func isSet(s string) bool { + return len(s) > 0 +} diff --git a/pkg/provision/civo.go b/pkg/provision/civo.go index af4c8c27..5038225a 100644 --- a/pkg/provision/civo.go +++ b/pkg/provision/civo.go @@ -6,6 +6,7 @@ package provision import ( "encoding/json" "fmt" + "io" "io/ioutil" "log" "net/http" @@ -22,12 +23,12 @@ type CivoProvisioner struct { // NewCivoProvisioner with an accessKey func NewCivoProvisioner(accessKey string) (*CivoProvisioner, error) { - return &CivoProvisioner{ APIKey: accessKey, }, nil } +// Status gets the status of the exit node func (p *CivoProvisioner) Status(id string) (*ProvisionedHost, error) { host := &ProvisionedHost{} @@ -40,7 +41,7 @@ func (p *CivoProvisioner) Status(id string) (*ProvisionedHost, error) { addAuth(req, p.APIKey) req.Header.Add("Accept", "application/json") - instance := CreatedInstance{} + instance := createdInstance{} res, err := http.DefaultClient.Do(req) if err != nil { @@ -69,41 +70,28 @@ func (p *CivoProvisioner) Status(id string) (*ProvisionedHost, error) { }, nil } -func (p *CivoProvisioner) Delete(id string) error { - - apiURL := fmt.Sprint("https://api.civo.com/v2/instances/", id) - - req, err := http.NewRequest(http.MethodDelete, apiURL, nil) - if err != nil { - return err +// Delete terminates the exit node +func (p *CivoProvisioner) Delete(request HostDeleteRequest) error { + var id string + var err error + if len(request.ID) > 0 { + id = request.ID + } else { + id, err = p.lookupID(request) + if err != nil { + return err + } } - addAuth(req, p.APIKey) - - req.Header.Add("Accept", "application/json") - instance := CreatedInstance{} - res, err := http.DefaultClient.Do(req) + apiURL := fmt.Sprint("https://api.civo.com/v2/instances/", id) + _, err = apiCall(p.APIKey, http.MethodDelete, apiURL, nil) if err != nil { return err } - - var body []byte - if res.Body != nil { - defer res.Body.Close() - body, _ = ioutil.ReadAll(res.Body) - } - - if res.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected HTTP code: %d\n%q", res.StatusCode, string(body)) - } - - unmarshalErr := json.Unmarshal(body, &instance) - if unmarshalErr != nil { - return unmarshalErr - } return nil } +// Provision creates a new exit node func (p *CivoProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { log.Printf("Provisioning host with Civo\n") @@ -123,8 +111,47 @@ func (p *CivoProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { }, nil } -func provisionCivoInstance(host BasicHost, key string) (CreatedInstance, error) { - instance := CreatedInstance{} +// List returns a list of exit nodes +func (p *CivoProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) { + var inlets []*ProvisionedHost + apiURL := fmt.Sprintf("https://api.civo.com/v2/instances/?tags=%s", filter.Filter) + body, err := apiCall(p.APIKey, http.MethodGet, apiURL, nil) + if err != nil { + return inlets, err + } + + var resp apiResponse + unmarshalErr := json.Unmarshal(body, &resp) + if unmarshalErr != nil { + return inlets, unmarshalErr + } + + for _, instance := range resp.Items { + host := &ProvisionedHost{ + IP: instance.PublicIP, + ID: instance.ID, + Status: instance.Status, + } + inlets = append(inlets, host) + } + return inlets, nil +} + +func (p *CivoProvisioner) lookupID(request HostDeleteRequest) (string, error) { + inlets, err := p.List(ListFilter{Filter: "inlets"}) + if err != nil { + return "", err + } + for _, inlet := range inlets { + if inlet.IP == request.IP { + return inlet.ID, nil + } + } + return "", fmt.Errorf("no host with ip: %s", request.IP) +} + +func provisionCivoInstance(host BasicHost, key string) (createdInstance, error) { + instance := createdInstance{} apiURL := "https://api.civo.com/v2/instances" @@ -137,10 +164,26 @@ func provisionCivoInstance(host BasicHost, key string) (CreatedInstance, error) values.Add("script", host.UserData) values.Add("tags", "inlets") - req, err := http.NewRequest(http.MethodPost, apiURL, strings.NewReader(values.Encode())) + body, err := apiCall(key, http.MethodPost, apiURL, strings.NewReader(values.Encode())) if err != nil { return instance, err } + + unmarshalErr := json.Unmarshal(body, &instance) + if unmarshalErr != nil { + return instance, unmarshalErr + } + + fmt.Printf("Instance ID: %s\n", instance.ID) + return instance, nil +} + +func apiCall(key, method, url string, requestBody io.Reader) ([]byte, error) { + + req, err := http.NewRequest(method, url, requestBody) + if err != nil { + return nil, err + } addAuth(req, key) req.Header.Add("Accept", "application/json") @@ -148,29 +191,30 @@ func provisionCivoInstance(host BasicHost, key string) (CreatedInstance, error) res, err := http.DefaultClient.Do(req) if err != nil { - return instance, err + return nil, err } var body []byte if res.Body != nil { defer res.Body.Close() - body, _ = ioutil.ReadAll(res.Body) + body, err = ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } } if res.StatusCode != http.StatusOK { - return instance, fmt.Errorf("unexpected HTTP code: %d\n%q", res.StatusCode, string(body)) + return nil, fmt.Errorf("unexpected HTTP code: %d\n%q", res.StatusCode, string(body)) } - unmarshalErr := json.Unmarshal(body, &instance) - if unmarshalErr != nil { - return instance, unmarshalErr - } + return body, nil +} - fmt.Printf("Instance ID: %s\n", instance.ID) - return instance, nil +type apiResponse struct { + Items []createdInstance `json:"items"` } -type CreatedInstance struct { +type createdInstance struct { ID string `json:"id"` CreatedAt time.Time `json:"created_at"` PublicIP string `json:"public_ip"` diff --git a/pkg/provision/digitalocean.go b/pkg/provision/digitalocean.go index 96bab713..da011f32 100644 --- a/pkg/provision/digitalocean.go +++ b/pkg/provision/digitalocean.go @@ -29,6 +29,7 @@ func NewDigitalOceanProvisioner(accessKey string) (*DigitalOceanProvisioner, err }, nil } +// Status returns the status of an exit node func (p *DigitalOceanProvisioner) Status(id string) (*ProvisionedHost, error) { sid, _ := strconv.Atoi(id) @@ -54,12 +55,72 @@ func (p *DigitalOceanProvisioner) Status(id string) (*ProvisionedHost, error) { }, nil } -func (p *DigitalOceanProvisioner) Delete(id string) error { - sid, _ := strconv.Atoi(id) - _, err := p.client.Droplets.Delete(context.Background(), sid) +// Delete terminates an exit node +func (p *DigitalOceanProvisioner) Delete(request HostDeleteRequest) error { + var id string + var err error + if len(request.ID) > 0 { + id = request.ID + } else { + id, err = p.lookupID(request) + if err != nil { + return err + } + } + sid, err := strconv.Atoi(id) + if err != nil { + return err + } + _, err = p.client.Droplets.Delete(context.Background(), sid) return err } +// List returns a list of exit nodes +func (p *DigitalOceanProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) { + var inlets []*ProvisionedHost + opt := &godo.ListOptions{} + for { + droplets, resp, err := p.client.Droplets.ListByTag(context.Background(), filter.Filter, opt) + if err != nil { + return inlets, err + } + for _, droplet := range droplets { + publicIP, err := droplet.PublicIPv4() + if err != nil { + return inlets, err + } + host := &ProvisionedHost{ + IP: publicIP, + ID: fmt.Sprintf("%d", droplet.ID), + } + inlets = append(inlets, host) + } + if resp.Links == nil || resp.Links.IsLastPage() { + break + } + page, err := resp.Links.CurrentPage() + if err != nil { + return inlets, err + } + opt.Page = page + 1 + } + return inlets, nil +} + +func (p *DigitalOceanProvisioner) lookupID(request HostDeleteRequest) (string, error) { + inlets, err := p.List(ListFilter{Filter: "inlets"}) + if err != nil { + return "", err + } + for _, inlet := range inlets { + if inlet.IP == request.IP { + return inlet.ID, nil + } + } + return "", fmt.Errorf("no host with ip: %s", request.IP) +} + +// Provision creates an exit node func (p *DigitalOceanProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { log.Printf("Provisioning host with DigitalOcean\n") @@ -75,6 +136,7 @@ func (p *DigitalOceanProvisioner) Provision(host BasicHost) (*ProvisionedHost, e Image: godo.DropletCreateImage{ Slug: host.OS, }, + Tags: []string{"inlets"}, UserData: host.UserData, } @@ -89,10 +151,12 @@ func (p *DigitalOceanProvisioner) Provision(host BasicHost) (*ProvisionedHost, e }, nil } +// TokenSource contains an access token type TokenSource struct { AccessToken string } +// Token returns an oauth2 token func (t *TokenSource) Token() (*oauth2.Token, error) { token := &oauth2.Token{ AccessToken: t.AccessToken, diff --git a/pkg/provision/ec2.go b/pkg/provision/ec2.go index a57a43c3..17add121 100644 --- a/pkg/provision/ec2.go +++ b/pkg/provision/ec2.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/aws/aws-sdk-go/aws/credentials" "strconv" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -16,7 +17,7 @@ type EC2Provisioner struct { ec2Provisioner *ec2.EC2 } -// NewEC2Provioner creates an EC2Provisioner and initialises an EC2 client +// NewEC2Provisioner creates an EC2Provisioner and initialises an EC2 client func NewEC2Provisioner(region, accessKey, secretKey string) (*EC2Provisioner, error) { sess, err := session.NewSession(&aws.Config{ Region: aws.String(region), @@ -26,7 +27,7 @@ func NewEC2Provisioner(region, accessKey, secretKey string) (*EC2Provisioner, er return &EC2Provisioner{ec2Provisioner: svc}, err } -// Provision deploys an exit node into AWS +// Provision deploys an exit node into AWS EC2 func (p *EC2Provisioner) Provision(host BasicHost) (*ProvisionedHost, error) { image, err := p.lookupAMI(host.OS) if err != nil { @@ -124,7 +125,18 @@ func (p *EC2Provisioner) Status(id string) (*ProvisionedHost, error) { } // Delete removes the exit node -func (p *EC2Provisioner) Delete(id string) error { +func (p *EC2Provisioner) Delete(request HostDeleteRequest) error { + var id string + var err error + if len(request.ID) > 0 { + id = request.ID + } else { + id, err = p.lookupID(request) + if err != nil { + return err + } + } + i, err := p.ec2Provisioner.DescribeInstances(&ec2.DescribeInstancesInput{ InstanceIds: []*string{aws.String(id)}, }) @@ -160,6 +172,63 @@ func (p *EC2Provisioner) Delete(id string) error { return nil } +// List returns a list of exit nodes +func (p *EC2Provisioner) List(filter ListFilter) ([]*ProvisionedHost, error) { + var inlets []*ProvisionedHost + var nextToken *string + filterValues := strings.Split(filter.Filter, ",") + for { + instances, err := p.ec2Provisioner.DescribeInstances(&ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String(filterValues[0]), + Values: []*string{aws.String(filterValues[1])}, + }, + }, + NextToken: nextToken, + }) + if err != nil { + return nil, err + } + for _, r := range instances.Reservations { + for _, i := range r.Instances { + if *i.State.Name != ec2.InstanceStateNameTerminated { + host := &ProvisionedHost{ + ID: *i.InstanceId, + } + if i.PublicIpAddress != nil { + host.IP = *i.PublicIpAddress + } + inlets = append(inlets, host) + } + } + } + nextToken = instances.NextToken + if nextToken == nil { + break + } + } + return inlets, nil +} + +func (p *EC2Provisioner) lookupID(request HostDeleteRequest) (string, error) { + inlets, err := p.List(ListFilter{ + Filter: "tag:inlets,exit-node", + ProjectID: request.ProjectID, + Zone: request.Zone, + }) + if err != nil { + return "", err + } + + for _, inlet := range inlets { + if inlet.IP == request.IP { + return inlet.ID, nil + } + } + return "", fmt.Errorf("no host with ip: %s", request.IP) +} + // creteEC2SecurityGroup creates a security group for the exit-node func (p *EC2Provisioner) creteEC2SecurityGroup(controlPort int, pro string) (*string, *string, error) { ports := []int{80, 443, controlPort} diff --git a/pkg/provision/gce.go b/pkg/provision/gce.go index 718c3e3d..129d9c85 100644 --- a/pkg/provision/gce.go +++ b/pkg/provision/gce.go @@ -10,6 +10,8 @@ import ( "google.golang.org/api/option" ) +const gceHostRunning = "RUNNING" + // GCEProvisioner holds reference to the compute service to provision compute resources type GCEProvisioner struct { gceProvisioner *compute.Service @@ -24,7 +26,7 @@ func NewGCEProvisioner(accessKey string) (*GCEProvisioner, error) { } // Provision provisions a new GCE instance as an exit node -func (gce *GCEProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { +func (p *GCEProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { // instance auto restart on failure autoRestart := true instance := &compute.Instance{ @@ -56,6 +58,9 @@ func (gce *GCEProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { }, }, }, + Labels: map[string]string{ + "inlets": "exit-node", + }, Tags: &compute.Tags{ Items: []string{"http-server", "https-server", "inlets"}, }, @@ -85,27 +90,27 @@ func (gce *GCEProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { }, } - exists, _ := gce.checkInletsFirewallRuleExists(host.Additional["projectid"], host.Additional["firewall-name"], host.Additional["firewall-port"]) + exists, _ := p.checkInletsFirewallRuleExists(host.Additional["projectid"], host.Additional["firewall-name"], host.Additional["firewall-port"]) if !exists { - err := gce.createInletsFirewallRule(host.Additional["projectid"], host.Additional["firewall-name"], host.Additional["firewall-port"]) + err := p.createInletsFirewallRule(host.Additional["projectid"], host.Additional["firewall-name"], host.Additional["firewall-port"]) log.Println("inlets firewallRule does not exist") if err != nil { - return nil, fmt.Errorf("Could not create inlets firewall rule: %v", err) + return nil, fmt.Errorf("could not create inlets firewall rule: %v", err) } log.Printf("Creating inlets firewallRule opening port: %s\n", host.Additional["firewall-port"]) } else { log.Println("inlets firewallRule exists") } - op, err := gce.gceProvisioner.Instances.Insert(host.Additional["projectid"], host.Additional["zone"], instance).Do() + op, err := p.gceProvisioner.Instances.Insert(host.Additional["projectid"], host.Additional["zone"], instance).Do() if err != nil { return nil, fmt.Errorf("could not provision GCE instance: %v", err) } status := "" - if op.Status == "RUNNING" { + if op.Status == gceHostRunning { status = ActiveStatus } return &ProvisionedHost{ @@ -116,10 +121,10 @@ func (gce *GCEProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { } // checkInletsFirewallRuleExists checks if the inlets firewall rule exists or not -func (gce *GCEProvisioner) checkInletsFirewallRuleExists(projectID string, firewallRuleName string, inletsPort string) (bool, error) { - op, err := gce.gceProvisioner.Firewalls.Get(projectID, firewallRuleName).Do() +func (p *GCEProvisioner) checkInletsFirewallRuleExists(projectID string, firewallRuleName string, inletsPort string) (bool, error) { + op, err := p.gceProvisioner.Firewalls.Get(projectID, firewallRuleName).Do() if err != nil { - return false, fmt.Errorf("Could not get inlets firewall rule: %v", err) + return false, fmt.Errorf("could not get inlets firewall rule: %v", err) } if op.Name == firewallRuleName { for _, firewallRule := range op.Allowed { @@ -134,7 +139,7 @@ func (gce *GCEProvisioner) checkInletsFirewallRuleExists(projectID string, firew } // createInletsFirewallRule creates a firewall rule opening up the control port for inlets -func (gce *GCEProvisioner) createInletsFirewallRule(projectID string, firewallRuleName string, inletsPort string) error { +func (p *GCEProvisioner) createInletsFirewallRule(projectID string, firewallRuleName string, inletsPort string) error { firewallRule := &compute.Firewall{ Name: firewallRuleName, Description: "Firewall rule created by inlets-operator", @@ -150,7 +155,7 @@ func (gce *GCEProvisioner) createInletsFirewallRule(projectID string, firewallRu TargetTags: []string{"inlets"}, } - _, err := gce.gceProvisioner.Firewalls.Insert(projectID, firewallRule).Do() + _, err := p.gceProvisioner.Firewalls.Insert(projectID, firewallRule).Do() if err != nil { return fmt.Errorf("could not create firewall rule: %v", err) } @@ -158,28 +163,92 @@ func (gce *GCEProvisioner) createInletsFirewallRule(projectID string, firewallRu } // Delete deletes the GCE exit node -func (gce *GCEProvisioner) Delete(id string) error { - instanceName, zone, projectID, err := getGCEFieldsFromID(id) - if err != nil { - return fmt.Errorf("Could not get custom GCE fields: %v", err) +func (p *GCEProvisioner) Delete(request HostDeleteRequest) error { + var instanceName string + var err error + if len(request.ID) > 0 { + instanceName, _, _, err = getGCEFieldsFromID(request.ID) + if err != nil { + return err + } + } else { + inletID, err := p.lookupID(request) + if err != nil { + return err + } + instanceName, _, _, err = getGCEFieldsFromID(inletID) + if err != nil { + return err + } } - _, err = gce.gceProvisioner.Instances.Delete(projectID, zone, instanceName).Do() + _, err = p.gceProvisioner.Instances.Delete(request.ProjectID, request.Zone, instanceName).Do() if err != nil { - return fmt.Errorf("Could not delete the GCE instance: %v", err) + return fmt.Errorf("could not delete the GCE instance: %v", err) } return err } +// List returns a list of exit nodes +func (p *GCEProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) { + var inlets []*ProvisionedHost + var pageToken string + for { + call := p.gceProvisioner.Instances.List(filter.ProjectID, filter.Zone).Filter(filter.Filter) + if len(pageToken) > 0 { + call = call.PageToken(pageToken) + } + + instances, err := call.Do() + if err != nil { + return inlets, fmt.Errorf("could not list instances: %v", err) + } + for _, instance := range instances.Items { + var status string + if instance.Status == gceHostRunning { + status = ActiveStatus + } + host := &ProvisionedHost{ + IP: instance.NetworkInterfaces[0].AccessConfigs[0].NatIP, + ID: constructCustomGCEID(instance.Name, filter.Zone, filter.ProjectID), + Status: status, + } + inlets = append(inlets, host) + } + if len(instances.NextPageToken) == 0 { + break + } + } + return inlets, nil +} + +func (p *GCEProvisioner) lookupID(request HostDeleteRequest) (string, error) { + inlets, err := p.List(ListFilter{ + Filter: "labels.inlets=exit-node", + ProjectID: request.ProjectID, + Zone: request.Zone, + }) + if err != nil { + return "", err + } + + for _, inlet := range inlets { + if inlet.IP == request.IP { + return inlet.ID, nil + } + } + return "", fmt.Errorf("no host with ip: %s", request.IP) +} + // Status checks the status of the provisioning GCE exit node -func (gce *GCEProvisioner) Status(id string) (*ProvisionedHost, error) { +func (p *GCEProvisioner) Status(id string) (*ProvisionedHost, error) { instanceName, zone, projectID, err := getGCEFieldsFromID(id) if err != nil { - return nil, fmt.Errorf("Could not get custom GCE fields: %v", err) + return nil, fmt.Errorf("could not get custom GCE fields: %v", err) } - op, err := gce.gceProvisioner.Instances.Get(projectID, zone, instanceName).Do() + op, err := p.gceProvisioner.Instances.Get(projectID, zone, instanceName).Do() if err != nil { - return nil, fmt.Errorf("Could not get instance: %v", err) + return nil, fmt.Errorf("could not get instance: %v", err) } status := "" diff --git a/pkg/provision/packet.go b/pkg/provision/packet.go index f69a195c..fbfc2ccb 100644 --- a/pkg/provision/packet.go +++ b/pkg/provision/packet.go @@ -1,12 +1,13 @@ package provision import ( + "fmt" "net/http" "github.com/packethost/packngo" ) -// PacketProvisioner provision a host on Packet.com +// PacketProvisioner provision a host on packet.com type PacketProvisioner struct { client *packngo.Client } @@ -18,6 +19,7 @@ func NewPacketProvisioner(accessKey string) (*PacketProvisioner, error) { }, nil } +// Status returns the IP, ID and Status of the exit node func (p *PacketProvisioner) Status(id string) (*ProvisionedHost, error) { device, _, err := p.client.Devices.Get(id, nil) @@ -42,11 +44,23 @@ func (p *PacketProvisioner) Status(id string) (*ProvisionedHost, error) { }, nil } -func (p *PacketProvisioner) Delete(id string) error { - _, err := p.client.Devices.Delete(id) +// Delete terminates the exit node +func (p *PacketProvisioner) Delete(request HostDeleteRequest) error { + var id string + var err error + if len(request.ID) > 0 { + id = request.ID + } else { + id, err = p.lookupID(request) + if err != nil { + return err + } + } + _, err = p.client.Devices.Delete(id) return err } +// Provision deploys an exit node into packet.com func (p *PacketProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { if host.Region == "" { host.Region = "ams1" @@ -61,6 +75,7 @@ func (p *PacketProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) OS: host.OS, BillingCycle: "hourly", UserData: host.UserData, + Tags: []string{"inlets"}, } device, _, err := p.client.Devices.Create(createReq) @@ -73,3 +88,38 @@ func (p *PacketProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) ID: device.ID, }, nil } + +// List returns a list of exit nodes +func (p *PacketProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) { + var inlets []*ProvisionedHost + devices, _, err := p.client.Devices.List(filter.ProjectID, nil) + if err != nil { + return nil, err + } + for _, device := range devices { + for _, tag := range device.Tags { + if tag == filter.Filter { + net := device.GetNetworkInfo() + host := &ProvisionedHost{ + IP: net.PublicIPv4, + ID: device.ID, + } + inlets = append(inlets, host) + } + } + } + return inlets, nil +} + +func (p *PacketProvisioner) lookupID(request HostDeleteRequest) (string, error) { + inlets, err := p.List(ListFilter{Filter: "inlets", ProjectID: request.ProjectID}) + if err != nil { + return "", err + } + for _, inlet := range inlets { + if inlet.IP == request.IP { + return inlet.ID, nil + } + } + return "", fmt.Errorf("no host with ip: %s", request.IP) +} diff --git a/pkg/provision/provision.go b/pkg/provision/provision.go index 06e2e5a7..60b7dfc5 100644 --- a/pkg/provision/provision.go +++ b/pkg/provision/provision.go @@ -1,19 +1,23 @@ package provision +// Provisioner is an interface used for deploying exit nodes into cloud providers type Provisioner interface { Provision(BasicHost) (*ProvisionedHost, error) Status(id string) (*ProvisionedHost, error) - Delete(id string) error + Delete(HostDeleteRequest) error } +// ActiveStatus is the status returned by an active exit node const ActiveStatus = "active" +// ProvisionedHost contains the IP, ID and Status of an exit node type ProvisionedHost struct { IP string ID string Status string } +// BasicHost contains the data required to deploy a exit node type BasicHost struct { Region string Plan string @@ -22,3 +26,18 @@ type BasicHost struct { UserData string Additional map[string]string } + +// HostDeleteRequest contains the data required to delete an exit node by either IP or ID +type HostDeleteRequest struct { + ID string + IP string + ProjectID string + Zone string +} + +// ListFilter is used to filter results to return only exit nodes +type ListFilter struct { + Filter string + ProjectID string + Zone string +} diff --git a/pkg/provision/scaleway.go b/pkg/provision/scaleway.go index 452dc1b7..7dc6173d 100644 --- a/pkg/provision/scaleway.go +++ b/pkg/provision/scaleway.go @@ -1,6 +1,7 @@ package provision import ( + "fmt" "log" "strings" "time" @@ -41,7 +42,7 @@ func NewScalewayProvisioner(accessKey, secretKey, organizationID, region string) // Provision creates a new scaleway instance from the BasicHost type // To provision the instance we first create the server, then set its user-data and power it on -func (s *ScalewayProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { +func (p *ScalewayProvisioner) Provision(host BasicHost) (*ProvisionedHost, error) { log.Printf("Provisioning host with Scaleway\n") if host.OS == "" { @@ -52,10 +53,11 @@ func (s *ScalewayProvisioner) Provision(host BasicHost) (*ProvisionedHost, error host.Plan = "DEV1-S" } - res, err := s.instanceAPI.CreateServer(&instance.CreateServerRequest{ + res, err := p.instanceAPI.CreateServer(&instance.CreateServerRequest{ Name: host.Name, CommercialType: host.Plan, Image: host.OS, + Tags: []string{"inlets"}, // DynamicIPRequired is mandatory to get a public IP // otherwise scaleway doesn't attach a public IP to the instance DynamicIPRequired: scw.BoolPtr(true), @@ -67,7 +69,7 @@ func (s *ScalewayProvisioner) Provision(host BasicHost) (*ProvisionedHost, error server := res.Server - err = s.instanceAPI.SetServerUserData(&instance.SetServerUserDataRequest{ + err = p.instanceAPI.SetServerUserData(&instance.SetServerUserDataRequest{ ServerID: server.ID, Key: "cloud-init", Content: strings.NewReader(host.UserData), @@ -77,7 +79,7 @@ func (s *ScalewayProvisioner) Provision(host BasicHost) (*ProvisionedHost, error return nil, err } - _, err = s.instanceAPI.ServerAction(&instance.ServerActionRequest{ + _, err = p.instanceAPI.ServerAction(&instance.ServerActionRequest{ ServerID: server.ID, Action: instance.ServerActionPoweron, }) @@ -91,8 +93,8 @@ func (s *ScalewayProvisioner) Provision(host BasicHost) (*ProvisionedHost, error } // Status returns the status of the scaleway instance -func (s *ScalewayProvisioner) Status(id string) (*ProvisionedHost, error) { - res, err := s.instanceAPI.GetServer(&instance.GetServerRequest{ +func (p *ScalewayProvisioner) Status(id string) (*ProvisionedHost, error) { + res, err := p.instanceAPI.GetServer(&instance.GetServerRequest{ ServerID: id, }) @@ -107,12 +109,22 @@ func (s *ScalewayProvisioner) Status(id string) (*ProvisionedHost, error) { // We should first poweroff the instance, // otherwise we'll have: http error 400 Bad Request: instance should be powered off. // Then we have to delete the server and attached volumes -func (s *ScalewayProvisioner) Delete(id string) error { - server, err := s.instanceAPI.GetServer(&instance.GetServerRequest{ +func (p *ScalewayProvisioner) Delete(request HostDeleteRequest) error { + var id string + var err error + if len(request.ID) > 0 { + id = request.ID + } else { + id, err = p.lookupID(request) + if err != nil { + return err + } + } + server, err := p.instanceAPI.GetServer(&instance.GetServerRequest{ ServerID: id, }) - err = s.instanceAPI.ServerActionAndWait(&instance.ServerActionAndWaitRequest{ + err = p.instanceAPI.ServerActionAndWait(&instance.ServerActionAndWaitRequest{ ServerID: id, Action: instance.ServerActionPoweroff, Timeout: 5 * time.Minute, @@ -122,7 +134,7 @@ func (s *ScalewayProvisioner) Delete(id string) error { return err } - err = s.instanceAPI.DeleteServer(&instance.DeleteServerRequest{ + err = p.instanceAPI.DeleteServer(&instance.DeleteServerRequest{ ServerID: id, }) @@ -131,7 +143,7 @@ func (s *ScalewayProvisioner) Delete(id string) error { } for _, volume := range server.Server.Volumes { - err := s.instanceAPI.DeleteVolume(&instance.DeleteVolumeRequest{ + err := p.instanceAPI.DeleteVolume(&instance.DeleteVolumeRequest{ VolumeID: volume.ID, }) @@ -143,6 +155,50 @@ func (s *ScalewayProvisioner) Delete(id string) error { return nil } +// List returns a list of exit nodes +func (p *ScalewayProvisioner) List(filter ListFilter) ([]*ProvisionedHost, error) { + var inlets []*ProvisionedHost + page := int32(1) + perPage := uint32(20) + for { + servers, err := p.instanceAPI.ListServers(&instance.ListServersRequest{Page: &page, PerPage: &perPage}) + if err != nil { + return inlets, err + } + for _, server := range servers.Servers { + for _, t := range server.Tags { + if t == filter.Filter { + host := &ProvisionedHost{ + IP: server.PublicIP.Address.String(), + ID: server.ID, + Status: server.State.String(), + } + inlets = append(inlets, host) + } + } + } + if len(servers.Servers) < int(perPage) { + break + } + page = page + 1 + } + return inlets, nil +} + +func (p *ScalewayProvisioner) lookupID(request HostDeleteRequest) (string, error) { + inlets, err := p.List(ListFilter{Filter: "inlets"}) + if err != nil { + return "", err + } + for _, inlet := range inlets { + if inlet.IP == request.IP { + return inlet.ID, nil + } + } + + return "", fmt.Errorf("no host with ip: %s", request.IP) +} + func serverToProvisionedHost(server *instance.Server) *ProvisionedHost { var ip = "" if server.PublicIP != nil {