From 7320a2cbf2211edee6ed2a0c9781f5687276a4a7 Mon Sep 17 00:00:00 2001 From: Kiril Vladimirov Date: Tue, 5 Aug 2014 12:50:21 +0300 Subject: [PATCH] feat(datasource/metadata): Add datasource for CloudSigma --- coreos-cloudinit.go | 20 ++- .../metadata/cloudsigma/server_context.go | 141 ++++++++++++++++ .../cloudsigma/server_context_test.go | 152 ++++++++++++++++++ 3 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 datasource/metadata/cloudsigma/server_context.go create mode 100644 datasource/metadata/cloudsigma/server_context_test.go diff --git a/coreos-cloudinit.go b/coreos-cloudinit.go index be5023d..f7caa6d 100644 --- a/coreos-cloudinit.go +++ b/coreos-cloudinit.go @@ -10,6 +10,7 @@ import ( "github.com/coreos/coreos-cloudinit/datasource" "github.com/coreos/coreos-cloudinit/datasource/configdrive" "github.com/coreos/coreos-cloudinit/datasource/file" + "github.com/coreos/coreos-cloudinit/datasource/metadata/cloudsigma" "github.com/coreos/coreos-cloudinit/datasource/metadata/ec2" "github.com/coreos/coreos-cloudinit/datasource/proc_cmdline" "github.com/coreos/coreos-cloudinit/datasource/url" @@ -29,12 +30,13 @@ var ( printVersion bool ignoreFailure bool sources struct { - file string - configDrive string - metadataService bool - ec2MetadataService string - url string - procCmdLine bool + file string + configDrive string + metadataService bool + ec2MetadataService string + cloudSigmaMetadataService bool + url string + procCmdLine bool } convertNetconf string workspace string @@ -48,6 +50,7 @@ func init() { flag.StringVar(&sources.configDrive, "from-configdrive", "", "Read data from provided cloud-drive directory") flag.BoolVar(&sources.metadataService, "from-metadata-service", false, "[DEPRECATED - Use -from-ec2-metadata] Download data from metadata service") flag.StringVar(&sources.ec2MetadataService, "from-ec2-metadata", "", "Download data from the provided metadata service") + flag.BoolVar(&sources.cloudSigmaMetadataService, "from-cloudsigma-metadata", false, "Download data from CloudSigma server context") flag.StringVar(&sources.url, "from-url", "", "Download user-data from provided url") flag.BoolVar(&sources.procCmdLine, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=', using the cloud-config served by an HTTP GET to ", proc_cmdline.ProcCmdlineLocation, proc_cmdline.ProcCmdlineCloudConfigFlag)) flag.StringVar(&convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files (requires the -from-configdrive flag)") @@ -85,7 +88,7 @@ func main() { dss := getDatasources() if len(dss) == 0 { - fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --from-url or --from-proc-cmdline") + fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --from-cloudsigma-metadata, --from-url or --from-proc-cmdline") os.Exit(1) } @@ -217,6 +220,9 @@ func getDatasources() []datasource.Datasource { if sources.ec2MetadataService != "" { dss = append(dss, ec2.NewDatasource(sources.ec2MetadataService)) } + if sources.cloudSigmaMetadataService { + dss = append(dss, cloudsigma.NewServerContextService()) + } if sources.procCmdLine { dss = append(dss, proc_cmdline.NewDatasource()) } diff --git a/datasource/metadata/cloudsigma/server_context.go b/datasource/metadata/cloudsigma/server_context.go new file mode 100644 index 0000000..4bd9d0b --- /dev/null +++ b/datasource/metadata/cloudsigma/server_context.go @@ -0,0 +1,141 @@ +package cloudsigma + +import ( + "encoding/base64" + "encoding/json" + "os" + "strings" + + "github.com/coreos/coreos-cloudinit/third_party/github.com/cloudsigma/cepgo" +) + +const ( + userDataFieldName = "cloudinit-user-data" +) + +type serverContextService struct { + client interface { + All() (interface{}, error) + Key(string) (interface{}, error) + Meta() (map[string]string, error) + FetchRaw(string) ([]byte, error) + } +} + +func NewServerContextService() *serverContextService { + return &serverContextService{ + client: cepgo.NewCepgo(), + } +} + +func (_ *serverContextService) IsAvailable() bool { + productNameFile, err := os.Open("/sys/class/dmi/id/product_name") + if err != nil { + return false + } + productName := make([]byte, 10) + _, err = productNameFile.Read(productName) + return err == nil && string(productName) == "CloudSigma" +} + +func (_ *serverContextService) AvailabilityChanges() bool { + return true +} + +func (_ *serverContextService) ConfigRoot() string { + return "" +} + +func (_ *serverContextService) Type() string { + return "server-context" +} + +func (scs *serverContextService) FetchMetadata() ([]byte, error) { + var ( + inputMetadata struct { + Name string `json:"name"` + UUID string `json:"uuid"` + Meta map[string]string `json:"meta"` + Nics []struct { + Runtime struct { + InterfaceType string `json:"interface_type"` + IPv4 struct { + IP string `json:"uuid"` + } `json:"ip_v4"` + } `json:"runtime"` + } `json:"nics"` + } + outputMetadata struct { + Hostname string `json:"name"` + PublicKeys map[string]string `json:"public_keys"` + LocalIPv4 string `json:"local-ipv4"` + PublicIPv4 string `json:"public-ipv4"` + } + ) + + rawMetadata, err := scs.client.FetchRaw("") + if err != nil { + return []byte{}, err + } + + err = json.Unmarshal(rawMetadata, &inputMetadata) + if err != nil { + return []byte{}, err + } + + if inputMetadata.Name != "" { + outputMetadata.Hostname = inputMetadata.Name + } else { + outputMetadata.Hostname = inputMetadata.UUID + } + + if key, ok := inputMetadata.Meta["ssh_public_key"]; ok { + splitted := strings.Split(key, " ") + outputMetadata.PublicKeys = make(map[string]string) + outputMetadata.PublicKeys[splitted[len(splitted)-1]] = key + } + + for _, nic := range inputMetadata.Nics { + if nic.Runtime.IPv4.IP != "" { + if nic.Runtime.InterfaceType == "public" { + outputMetadata.PublicIPv4 = nic.Runtime.IPv4.IP + } else { + outputMetadata.LocalIPv4 = nic.Runtime.IPv4.IP + } + } + } + + return json.Marshal(outputMetadata) +} + +func (scs *serverContextService) FetchUserdata() ([]byte, error) { + metadata, err := scs.client.Meta() + if err != nil { + return []byte{}, err + } + + userData, ok := metadata[userDataFieldName] + if ok && isBase64Encoded(userDataFieldName, metadata) { + if decodedUserData, err := base64.StdEncoding.DecodeString(userData); err == nil { + return decodedUserData, nil + } else { + return []byte{}, nil + } + } + + return []byte(userData), nil +} + +func isBase64Encoded(field string, userdata map[string]string) bool { + base64Fields, ok := userdata["base64_fields"] + if !ok { + return false + } + + for _, base64Field := range strings.Split(base64Fields, ",") { + if field == base64Field { + return true + } + } + return false +} diff --git a/datasource/metadata/cloudsigma/server_context_test.go b/datasource/metadata/cloudsigma/server_context_test.go new file mode 100644 index 0000000..39332c3 --- /dev/null +++ b/datasource/metadata/cloudsigma/server_context_test.go @@ -0,0 +1,152 @@ +package cloudsigma + +import ( + "encoding/json" + "reflect" + "testing" +) + +type fakeCepgoClient struct { + raw []byte + meta map[string]string + keys map[string]interface{} + err error +} + +func (f *fakeCepgoClient) All() (interface{}, error) { + return f.keys, f.err +} + +func (f *fakeCepgoClient) Key(key string) (interface{}, error) { + return f.keys[key], f.err +} + +func (f *fakeCepgoClient) Meta() (map[string]string, error) { + return f.meta, f.err +} + +func (f *fakeCepgoClient) FetchRaw(key string) ([]byte, error) { + return f.raw, f.err +} + +func TestServerContextFetchMetadata(t *testing.T) { + var metadata struct { + Hostname string `json:"name"` + PublicKeys map[string]string `json:"public_keys"` + LocalIPv4 string `json:"local-ipv4"` + PublicIPv4 string `json:"public-ipv4"` + } + client := new(fakeCepgoClient) + scs := NewServerContextService() + scs.client = client + client.raw = []byte(`{ + "context": true, + "cpu": 4000, + "cpu_model": null, + "cpus_instead_of_cores": false, + "enable_numa": false, + "grantees": [], + "hv_relaxed": false, + "hv_tsc": false, + "jobs": [], + "mem": 4294967296, + "meta": { + "base64_fields": "cloudinit-user-data", + "cloudinit-user-data": "I2Nsb3VkLWNvbmZpZwoKaG9zdG5hbWU6IGNvcmVvczE=", + "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2E.../hQ5D5 john@doe" + }, + "name": "coreos", + "nics": [ + { + "runtime": { + "interface_type": "public", + "ip_v4": { + "uuid": "31.171.251.74" + }, + "ip_v6": null + }, + "vlan": null + } + ], + "smp": 2, + "status": "running", + "uuid": "20a0059b-041e-4d0c-bcc6-9b2852de48b3" + }`) + + metadataBytes, err := scs.FetchMetadata() + if err != nil { + t.Error(err.Error()) + } + + if err := json.Unmarshal(metadataBytes, &metadata); err != nil { + t.Error(err.Error()) + } + + if metadata.Hostname != "coreos" { + t.Errorf("Hostname is not 'coreos' but %s instead", metadata.Hostname) + } + + if metadata.PublicKeys["john@doe"] != "ssh-rsa AAAAB3NzaC1yc2E.../hQ5D5 john@doe" { + t.Error("Public SSH Keys are not being read properly") + } + + if metadata.LocalIPv4 != "" { + t.Errorf("Local IP is not empty but %s instead", metadata.LocalIPv4) + } + + if metadata.PublicIPv4 != "31.171.251.74" { + t.Errorf("Local IP is not 31.171.251.74 but %s instead", metadata.PublicIPv4) + } +} + +func TestServerContextFetchUserdata(t *testing.T) { + client := new(fakeCepgoClient) + scs := NewServerContextService() + scs.client = client + userdataSets := []struct { + in map[string]string + err bool + out []byte + }{ + {map[string]string{ + "base64_fields": "cloudinit-user-data", + "cloudinit-user-data": "aG9zdG5hbWU6IGNvcmVvc190ZXN0", + }, false, []byte("hostname: coreos_test")}, + {map[string]string{ + "cloudinit-user-data": "#cloud-config\\nhostname: coreos1", + }, false, []byte("#cloud-config\\nhostname: coreos1")}, + {map[string]string{}, false, []byte{}}, + } + + for i, set := range userdataSets { + client.meta = set.in + got, err := scs.FetchUserdata() + if (err != nil) != set.err { + t.Errorf("case %d: bad error state (got %t, want %t)", i, err != nil, set.err) + } + + if !reflect.DeepEqual(got, set.out) { + t.Errorf("case %d: got %s, want %s", i, got, set.out) + } + } +} + +func TestServerContextDecodingBase64UserData(t *testing.T) { + base64Sets := []struct { + in string + out bool + }{ + {"cloudinit-user-data,foo,bar", true}, + {"bar,cloudinit-user-data,foo,bar", true}, + {"cloudinit-user-data", true}, + {"", false}, + {"foo", false}, + } + + for _, set := range base64Sets { + userdata := map[string]string{"base64_fields": set.in} + if isBase64Encoded("cloudinit-user-data", userdata) != set.out { + t.Errorf("isBase64Encoded(cloudinit-user-data, %s) should be %t", userdata, set.out) + } + } +}