From 5ab6b88029bb17e6773185e566d35c79b0435bea Mon Sep 17 00:00:00 2001 From: Patrik Cyvoct Date: Thu, 14 Jun 2018 11:06:01 +0200 Subject: [PATCH] Add Scaleway provider in metadata package Signed-off-by: Patrik Cyvoct --- pkg/metadata/build.yml | 1 + pkg/metadata/main.go | 4 +- pkg/metadata/provider_scaleway.go | 263 ++++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 pkg/metadata/provider_scaleway.go diff --git a/pkg/metadata/build.yml b/pkg/metadata/build.yml index cf0f1d552..30425e602 100644 --- a/pkg/metadata/build.yml +++ b/pkg/metadata/build.yml @@ -13,3 +13,4 @@ config: capabilities: - CAP_SYS_ADMIN - CAP_NET_ADMIN + - CAP_NET_BIND_SERVICE diff --git a/pkg/metadata/main.go b/pkg/metadata/main.go index 2f869aa73..8280b0fef 100644 --- a/pkg/metadata/main.go +++ b/pkg/metadata/main.go @@ -45,7 +45,7 @@ var netProviders []Provider var cdromProviders []Provider func main() { - providers := []string{"aws", "gcp", "openstack", "vultr", "packet", "cdrom"} + providers := []string{"aws", "gcp", "openstack", "scaleway", "vultr", "packet", "cdrom"} if len(os.Args) > 1 { providers = os.Args[1:] } @@ -59,6 +59,8 @@ func main() { netProviders = append(netProviders, NewOpenstack()) case "packet": netProviders = append(netProviders, NewPacket()) + case "scaleway": + netProviders = append(netProviders, NewScaleway()) case "vultr": netProviders = append(netProviders, NewVultr()) case "cdrom": diff --git a/pkg/metadata/provider_scaleway.go b/pkg/metadata/provider_scaleway.go new file mode 100644 index 000000000..1ea3880b2 --- /dev/null +++ b/pkg/metadata/provider_scaleway.go @@ -0,0 +1,263 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "path" + "strconv" + "strings" + "time" +) + +const ( + scalewayMetadataURL = "http://169.254.42.42/" + scalewayUserdataURL = "169.254.42.42:80" + instanceIDFile = "instance_id" + instanceLocationFile = "instance_location" + publicIPFile = "public_ip" + privateIPFile = "private_ip" +) + +// ProviderScaleway is the type implementing the Provider interface for Scaleway +type ProviderScaleway struct { +} + +// NewScaleway returns a new ProviderScaleway +func NewScaleway() *ProviderScaleway { + return &ProviderScaleway{} +} + +func (p *ProviderScaleway) String() string { + return "Scaleway" +} + +func (p *ProviderScaleway) sendBootSignal() error { + var client = &http.Client{ + Timeout: time.Second * 2, + } + + state := []byte(`{"state_detail": "booted"}`) + + req, err := http.NewRequest("PATCH", scalewayMetadataURL+"state", bytes.NewBuffer(state)) + if err != nil { + return fmt.Errorf("Scaleway: http.NewRequest failed: %s", err) + } + req.Header.Set("Content-Type", "application/json") + + _, err = client.Do(req) + if err != nil { + return fmt.Errorf("Scaleway: Could not contact state service: %s", err) + } + + return nil +} + +// Probe checks if we are running on Scaleway +func (p *ProviderScaleway) Probe() bool { + // Getting the conf should always work... + _, err := scalewayGet(scalewayMetadataURL + "conf") + if err != nil { + log.Printf(err.Error()) + return false + } + + // we are on Scaleway so we need to send a request to tell that the instance has correctly booted + err = p.sendBootSignal() + if err != nil { + log.Printf("Scaleway: Could not signal that the instance booted") + } + + return true +} + +// Extract gets both the Scaleway specific and generic userdata +func (p *ProviderScaleway) Extract() ([]byte, error) { + metadata, err := scalewayGet(scalewayMetadataURL + "conf") + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to get conf: %s", err) + } + + hostname, err := p.extractInformation(metadata, "hostname") + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to get hostname: %s", err) + } + + err = ioutil.WriteFile(path.Join(ConfigPath, Hostname), hostname, 0644) + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to write hostname: %s", err) + } + + instanceID, err := p.extractInformation(metadata, "id") + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to get instanceID: %s", err) + } + + err = ioutil.WriteFile(path.Join(ConfigPath, instanceIDFile), instanceID, 0644) + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to write instance_id: %s", err) + } + + instanceLocation, err := p.extractInformation(metadata, "location_zone_id") + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to get instanceLocation: %s", err) + } + + err = ioutil.WriteFile(path.Join(ConfigPath, instanceLocationFile), instanceLocation, 0644) + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to write instance_location: %s", err) + } + + publicIP, err := p.extractInformation(metadata, "public_ip_address") + if err != nil { + // not an error + log.Printf("Scaleway: Failed to get publicIP: %s", err) + } else { + err = ioutil.WriteFile(path.Join(ConfigPath, publicIPFile), publicIP, 0644) + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to write public_ip: %s", err) + } + + } + + privateIP, err := p.extractInformation(metadata, "private_ip") + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to get privateIP: %s", err) + } + + err = ioutil.WriteFile(path.Join(ConfigPath, privateIPFile), privateIP, 0644) + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to write private_ip: %s", err) + } + + if err := p.handleSSH(metadata); err != nil { + log.Printf("Scaleway: Failed to get ssh data: %s", err) + } + + // Generic userdata + userData, err := scalewayGetUserdata() + if err != nil { + log.Printf("Scaleway: Failed to get user-data: %s", err) + // This is not an error + return nil, nil + } + return userData, nil +} + +// exctractInformation returns the extracted information given as parameter from the metadata +func (p *ProviderScaleway) extractInformation(metadata []byte, information string) ([]byte, error) { + query := strings.ToUpper(information) + "=" + for _, line := range bytes.Split(metadata, []byte("\n")) { + if bytes.HasPrefix(line, []byte(query)) { + return bytes.TrimPrefix(line, []byte(query)), nil + } + } + return []byte(""), fmt.Errorf("No %s found", information) +} + +// scalewayGet requests and extracts the requested URL +func scalewayGet(url string) ([]byte, error) { + var client = &http.Client{ + Timeout: time.Second * 2, + } + + req, err := http.NewRequest("", url, nil) + if err != nil { + return nil, fmt.Errorf("Scaleway: http.NewRequest failed: %s", err) + } + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("Scaleway: Could not contact metadata service: %s", err) + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("Scaleway: Status not ok: %d", resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("Scaleway: Failed to read http response: %s", err) + } + return body, nil +} + +// scalewayGetUserdata returns the userdata of the server, differs from scalewayGet since the source port has to be below 1024 in order to work +func scalewayGetUserdata() ([]byte, error) { + server, err := net.ResolveTCPAddr("tcp", scalewayUserdataURL) + if err != nil { + return nil, err + } + var conn *net.TCPConn + foundPort := false + for i := 1; i <= 1024; i++ { + client, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(i)) + if err != nil { + return nil, err + } + + conn, err = net.DialTCP("tcp", client, server) + if err == nil { + foundPort = true + break + } + } + if foundPort == false { + return nil, errors.New("not able to found a free port below 1024") + } + defer conn.Close() + fmt.Fprintf(conn, "GET /user_data HTTP/1.0\r\n\r\n") + + reader := bufio.NewReader(conn) + resp, err := http.ReadResponse(reader, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} + +func (p *ProviderScaleway) handleSSH(metadata []byte) error { + sshKeysNumberString, err := p.extractInformation(metadata, "ssh_public_keys") + if err != nil { + return fmt.Errorf("Failed to get sshKeys: %s", err) + } + sshKeysNumber, err := strconv.Atoi(string(sshKeysNumberString)) + if err != nil { + return fmt.Errorf("Failed to convert sshKeysNumber to int: %s", err) + } + + rootKeys := "" + for i := 0; i < sshKeysNumber; i++ { + sshKey, err := p.extractInformation(metadata, "ssh_public_keys_"+strconv.Itoa(i)+"_key") + if err != nil { + return fmt.Errorf("Failed to get ssh_key %d: %s", i, err) + } + + line := string(bytes.Trim(sshKey, "'")) + parts := strings.SplitN(line, " ", 2) + if len(parts) == 2 { + rootKeys = rootKeys + parts[1] + "\n" + } + } + + if err := os.Mkdir(path.Join(ConfigPath, SSH), 0755); err != nil { + return fmt.Errorf("Failed to create %s: %s", SSH, err) + } + + err = ioutil.WriteFile(path.Join(ConfigPath, SSH, "authorized_keys"), []byte(rootKeys), 0600) + if err != nil { + return fmt.Errorf("Failed to write ssh keys: %s", err) + } + return nil +}