diff --git a/config/config.go b/config/config.go index 9278ecc..ac6eb51 100644 --- a/config/config.go +++ b/config/config.go @@ -38,12 +38,18 @@ type CloudConfig struct { Update Update `yaml:"update"` Units []Unit `yaml:"units"` } `yaml:"coreos"` - WriteFiles []File `yaml:"write_files"` - Hostname string `yaml:"hostname"` - Users []User `yaml:"users"` - ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"` - NetworkConfigPath string `yaml:"-"` - NetworkConfig string `yaml:"-"` + WriteFiles []File `yaml:"write_files"` + Debug bool `yaml:"debug"` + RunCMD []string `yaml:"runcmd"` + Hostname string `yaml:"hostname"` + Users []User `yaml:"users"` + ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"` + NetworkConfigPath string `yaml:"-"` + NetworkConfig string `yaml:"-"` + SystemInfo SystemInfo `yaml:"system_info"` + DisableRoot bool `yaml:"disable_root"` + SSHPasswdAuth bool `yaml:"ssh_pwauth"` + ResizeRootfs bool `yaml:"resize_rootfs"` } func IsCloudConfig(userdata string) bool { diff --git a/config/system_info.go b/config/system_info.go new file mode 100644 index 0000000..754df38 --- /dev/null +++ b/config/system_info.go @@ -0,0 +1,7 @@ +package config + +type SystemInfo struct { + DefaultUser struct { + Name string `yaml:"name"` + } `yaml:"default_user"` +} diff --git a/config/user.go b/config/user.go index 44e959c..331c374 100644 --- a/config/user.go +++ b/config/user.go @@ -30,4 +30,5 @@ type User struct { NoUserGroup bool `yaml:"no_user_group"` System bool `yaml:"system"` NoLogInit bool `yaml:"no_log_init"` + LockPasswd bool `yaml:"lock-passwd"` } diff --git a/coreos-cloudinit.go b/coreos-cloudinit.go index 7bde360..52c8acaa 100644 --- a/coreos-cloudinit.go +++ b/coreos-cloudinit.go @@ -31,6 +31,7 @@ import ( "github.com/coreos/coreos-cloudinit/datasource/metadata/cloudsigma" "github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean" "github.com/coreos/coreos-cloudinit/datasource/metadata/ec2" + "github.com/coreos/coreos-cloudinit/datasource/metadata/openstack" "github.com/coreos/coreos-cloudinit/datasource/proc_cmdline" "github.com/coreos/coreos-cloudinit/datasource/url" "github.com/coreos/coreos-cloudinit/datasource/waagent" @@ -58,6 +59,7 @@ var ( ec2MetadataService string cloudSigmaMetadataService bool digitalOceanMetadataService string + openstackMetadataService string url string procCmdLine bool } @@ -79,6 +81,7 @@ func init() { flag.StringVar(&flags.sources.ec2MetadataService, "from-ec2-metadata", "", "Download EC2 data from the provided url") flag.BoolVar(&flags.sources.cloudSigmaMetadataService, "from-cloudsigma-metadata", false, "Download data from CloudSigma server context") flag.StringVar(&flags.sources.digitalOceanMetadataService, "from-digitalocean-metadata", "", "Download DigitalOcean data from the provided url") + flag.StringVar(&flags.sources.openstackMetadataService, "from-openstack-metadata", "", "Download OpenStack data from the provided url") flag.StringVar(&flags.sources.url, "from-url", "", "Download user-data from provided url") flag.BoolVar(&flags.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(&flags.oem, "oem", "", "Use the settings specific to the provided OEM") @@ -100,6 +103,10 @@ var ( "from-ec2-metadata": "http://169.254.169.254/", "from-configdrive": "/media/configdrive", }, + "openstack": oemConfig{ + "from-openstack-metadata": "http://169.254.169.254/", + "convert-netconf": "debian", + }, "rackspace-onmetal": oemConfig{ "from-configdrive": "/media/configdrive", "convert-netconf": "debian", @@ -144,7 +151,7 @@ func main() { dss := getDatasources() if len(dss) == 0 { - fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --from-cloudsigma-metadata, --from-url or --from-proc-cmdline") + fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --from-digitalocean-metadata, --from-openstack-metadata --from-cloudsigma-metadata, --from-url or --from-proc-cmdline") os.Exit(2) } @@ -320,6 +327,9 @@ func getDatasources() []datasource.Datasource { if flags.sources.digitalOceanMetadataService != "" { dss = append(dss, digitalocean.NewDatasource(flags.sources.digitalOceanMetadataService)) } + if flags.sources.openstackMetadataService != "" { + dss = append(dss, openstack.NewDatasource(flags.sources.openstackMetadataService)) + } if flags.sources.waagent != "" { dss = append(dss, waagent.NewDatasource(flags.sources.waagent)) } diff --git a/datasource/metadata/openstack/metadata.go b/datasource/metadata/openstack/metadata.go new file mode 100644 index 0000000..c465ce6 --- /dev/null +++ b/datasource/metadata/openstack/metadata.go @@ -0,0 +1,125 @@ +/* + Copyright 2014 CoreOS, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package openstack + +import ( + "encoding/json" + + "github.com/coreos/coreos-cloudinit/datasource/metadata" +) + +const ( + DefaultAddress = "http://169.254.169.254/" + apiVersion = "openstack/latest" + userdataUrl = apiVersion + "/user_data" + metadataPath = apiVersion + "/meta_data.json" +) + +type Address struct { + IPAddress string `json:"ip_address"` + Netmask string `json:"netmask"` + Cidr int `json:"cidr"` + Gateway string `json:"gateway"` +} + +type Interface struct { + IPv4 *Address `json:"ipv4"` + IPv6 *Address `json:"ipv6"` + MAC string `json:"mac"` + Type string `json:"type"` +} + +type Interfaces struct { + Public []Interface `json:"public"` + Private []Interface `json:"private"` +} + +type DNS struct { + Nameservers []string `json:"nameservers"` +} + +type Metadata struct { + Hostname string `json:"hostname"` + Interfaces Interfaces `json:"interfaces,omitempty"` + PublicKeys map[string]string `json:"public_keys"` + DNS DNS `json:"dns,omitempty"` +} + +type metadataService struct { + interfaces Interfaces + dns DNS + metadata.MetadataService +} + +func NewDatasource(root string) *metadataService { + return &metadataService{MetadataService: metadata.NewDatasource(root, apiVersion, userdataUrl, metadataPath)} +} + +func (ms *metadataService) FetchMetadata() ([]byte, error) { + data, err := ms.FetchData(ms.MetadataUrl()) + if err != nil || len(data) == 0 { + return []byte{}, err + } + + var metadata Metadata + if err := json.Unmarshal(data, &metadata); err != nil { + return []byte{}, err + } + + ms.interfaces = metadata.Interfaces + ms.dns = metadata.DNS + + attrs := make(map[string]interface{}) + + if len(metadata.Interfaces.Public) > 0 || len(metadata.Interfaces.Private) > 0 { + if len(metadata.Interfaces.Public) > 0 { + if metadata.Interfaces.Public[0].IPv4 != nil { + attrs["public-ipv4"] = metadata.Interfaces.Public[0].IPv4.IPAddress + } + if metadata.Interfaces.Public[0].IPv6 != nil { + attrs["public-ipv6"] = metadata.Interfaces.Public[0].IPv6.IPAddress + } + } + if len(metadata.Interfaces.Private) > 0 { + if metadata.Interfaces.Private[0].IPv4 != nil { + attrs["local-ipv4"] = metadata.Interfaces.Private[0].IPv4.IPAddress + } + if metadata.Interfaces.Private[0].IPv6 != nil { + attrs["local-ipv6"] = metadata.Interfaces.Private[0].IPv6.IPAddress + } + } + } + attrs["hostname"] = metadata.Hostname + keys := make(map[string]string) + for name, key := range metadata.PublicKeys { + keys[name] = key + } + attrs["public_keys"] = keys + + return json.Marshal(attrs) +} + +func (ms metadataService) FetchNetworkConfig(filename string) ([]byte, error) { + return json.Marshal(Metadata{ + Interfaces: ms.interfaces, + DNS: ms.dns, + }) +} + +func (ms metadataService) Type() string { + return "openstack-metadata-service" +} diff --git a/datasource/metadata/openstack/metadata_test.go b/datasource/metadata/openstack/metadata_test.go new file mode 100644 index 0000000..3ddc62f --- /dev/null +++ b/datasource/metadata/openstack/metadata_test.go @@ -0,0 +1,115 @@ +/* + Copyright 2014 CoreOS, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package openstack + +import ( + "bytes" + "fmt" + "testing" + + "github.com/coreos/coreos-cloudinit/datasource/metadata" + "github.com/coreos/coreos-cloudinit/datasource/metadata/test" + "github.com/coreos/coreos-cloudinit/pkg" +) + +func TestType(t *testing.T) { + want := "openstack-metadata-service" + if kind := (metadataService{}).Type(); kind != want { + t.Fatalf("bad type: want %q, got %q", want, kind) + } +} + +func TestFetchMetadata(t *testing.T) { + for _, tt := range []struct { + root string + metadataPath string + resources map[string]string + expect []byte + clientErr error + expectErr error + }{ + { + root: "/", + metadataPath: "v1.json", + resources: map[string]string{ + "/v1.json": "bad", + }, + expectErr: fmt.Errorf("invalid character 'b' looking for beginning of value"), + }, + { + root: "/", + metadataPath: "v1.json", + resources: map[string]string{ + "/v1.json": `{ + "droplet_id": 1, + "user_data": "hello", + "vendor_data": "hello", + "public_keys": [ + "publickey1", + "publickey2" + ], + "region": "nyc2", + "interfaces": { + "public": [ + { + "ipv4": { + "ip_address": "192.168.1.2", + "netmask": "255.255.255.0", + "gateway": "192.168.1.1" + }, + "ipv6": { + "ip_address": "fe00::", + "cidr": 126, + "gateway": "fe00::" + }, + "mac": "ab:cd:ef:gh:ij", + "type": "public" + } + ] + } +}`, + }, + expect: []byte(`{"hostname":"","public-ipv4":"192.168.1.2","public-ipv6":"fe00::","public_keys":{"0":"publickey1","1":"publickey2"}}`), + }, + { + clientErr: pkg.ErrTimeout{Err: fmt.Errorf("test error")}, + expectErr: pkg.ErrTimeout{Err: fmt.Errorf("test error")}, + }, + } { + service := &metadataService{ + MetadataService: metadata.MetadataService{ + Root: tt.root, + Client: &test.HttpClient{Resources: tt.resources, Err: tt.clientErr}, + MetadataPath: tt.metadataPath, + }, + } + metadata, err := service.FetchMetadata() + if Error(err) != Error(tt.expectErr) { + t.Fatalf("bad error (%q): want %q, got %q", tt.resources, tt.expectErr, err) + } + if !bytes.Equal(metadata, tt.expect) { + t.Fatalf("bad fetch (%q): want %q, got %q", tt.resources, tt.expect, metadata) + } + } +} + +func Error(err error) string { + if err != nil { + return err.Error() + } + return "" +} diff --git a/units/90-configdrive.rules b/extra/systemd/90-configdrive.rules similarity index 100% rename from units/90-configdrive.rules rename to extra/systemd/90-configdrive.rules diff --git a/units/media-configdrive.mount b/extra/systemd/media-configdrive.mount similarity index 100% rename from units/media-configdrive.mount rename to extra/systemd/media-configdrive.mount diff --git a/units/media-configvirtfs.mount b/extra/systemd/media-configvirtfs.mount similarity index 100% rename from units/media-configvirtfs.mount rename to extra/systemd/media-configvirtfs.mount diff --git a/units/system-cloudinit@.service b/extra/systemd/system-cloudinit@.service similarity index 100% rename from units/system-cloudinit@.service rename to extra/systemd/system-cloudinit@.service diff --git a/units/system-config.target b/extra/systemd/system-config.target similarity index 100% rename from units/system-config.target rename to extra/systemd/system-config.target diff --git a/units/user-cloudinit-proc-cmdline.service b/extra/systemd/user-cloudinit-proc-cmdline.service similarity index 100% rename from units/user-cloudinit-proc-cmdline.service rename to extra/systemd/user-cloudinit-proc-cmdline.service diff --git a/units/user-cloudinit@.path b/extra/systemd/user-cloudinit@.path similarity index 100% rename from units/user-cloudinit@.path rename to extra/systemd/user-cloudinit@.path diff --git a/units/user-cloudinit@.service b/extra/systemd/user-cloudinit@.service similarity index 100% rename from units/user-cloudinit@.service rename to extra/systemd/user-cloudinit@.service diff --git a/units/user-config.target b/extra/systemd/user-config.target similarity index 100% rename from units/user-config.target rename to extra/systemd/user-config.target diff --git a/units/user-configdrive.path b/extra/systemd/user-configdrive.path similarity index 100% rename from units/user-configdrive.path rename to extra/systemd/user-configdrive.path diff --git a/units/user-configdrive.service b/extra/systemd/user-configdrive.service similarity index 100% rename from units/user-configdrive.service rename to extra/systemd/user-configdrive.service diff --git a/units/user-configvirtfs.service b/extra/systemd/user-configvirtfs.service similarity index 100% rename from units/user-configvirtfs.service rename to extra/systemd/user-configvirtfs.service diff --git a/initialize/config.go b/initialize/config.go index 3e9c64e..4834489 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -96,9 +96,9 @@ func Apply(cfg config.CloudConfig, env *Environment) error { } if len(cfg.SSHAuthorizedKeys) > 0 { - err := system.AuthorizeSSHKeys("core", env.SSHKeyName(), cfg.SSHAuthorizedKeys) + err := system.AuthorizeSSHKeys(cfg.SystemInfo.DefaultUser.Name, env.SSHKeyName(), cfg.SSHAuthorizedKeys) if err == nil { - log.Printf("Authorized SSH keys for core user") + log.Printf("Authorized SSH keys for %s user", cfg.SystemInfo.DefaultUser.Name) } else { return err } diff --git a/pkg/http_client.go b/pkg/http_client.go index 25e78ce..61c204a 100644 --- a/pkg/http_client.go +++ b/pkg/http_client.go @@ -81,7 +81,7 @@ type Getter interface { func NewHttpClient() *HttpClient { hc := &HttpClient{ MaxBackoff: time.Second * 5, - MaxRetries: 15, + MaxRetries: 15, //TODO(configure duration) Timeout: time.Duration(2) * time.Second, SkipTLS: false, } diff --git a/system/ssh_key.go b/system/ssh_key.go index fd7a82e..e4354b3 100644 --- a/system/ssh_key.go +++ b/system/ssh_key.go @@ -18,58 +18,35 @@ package system import ( "fmt" - "io" - "io/ioutil" - "os/exec" + "os" "strings" ) // Add the provide SSH public key to the core user's list of // authorized keys func AuthorizeSSHKeys(user string, keysName string, keys []string) error { - for i, key := range keys { - keys[i] = strings.TrimSpace(key) + for name, key := range keys { + keys[name] = strings.TrimSpace(key) } // join all keys with newlines, ensuring the resulting string // also ends with a newline joined := fmt.Sprintf("%s\n", strings.Join(keys, "\n")) - cmd := exec.Command("update-ssh-keys", "-u", user, "-a", keysName) - stdin, err := cmd.StdinPipe() + authorized_file := "" + switch user { + case "root": + authorized_file = "/root/.ssh/authorized_keys" + default: + authorized_file = fmt.Sprintf("/home/%s/.ssh/authorized_keys", user) + } + + f, err := os.OpenFile(authorized_file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } + defer f.Close() + _, err = f.WriteString(joined) - stdout, err := cmd.StdoutPipe() - if err != nil { - return err - } - - stderr, err := cmd.StderrPipe() - if err != nil { - return err - } - - err = cmd.Start() - if err != nil { - stdin.Close() - return err - } - - _, err = io.WriteString(stdin, joined) - if err != nil { - return err - } - - stdin.Close() - stdoutBytes, _ := ioutil.ReadAll(stdout) - stderrBytes, _ := ioutil.ReadAll(stderr) - - err = cmd.Wait() - if err != nil { - return fmt.Errorf("Call to update-ssh-keys failed with %v: %s %s", err, string(stdoutBytes), string(stderrBytes)) - } - - return nil + return err } diff --git a/system/user.go b/system/user.go index c8eba07..14d6ec1 100644 --- a/system/user.go +++ b/system/user.go @@ -79,6 +79,22 @@ func CreateUser(u *config.User) error { output, err := exec.Command("useradd", args...).CombinedOutput() if err != nil { log.Printf("Command 'useradd %s' failed: %v\n%s", strings.Join(args, " "), err, output) + return err + } + + args = []string{} + + if u.LockPasswd { + args = append(args, "--lock") + } else { + args = append(args, "--unlock") + } + + args = append(args, u.Name) + + output, err = exec.Command("passwd", args...).CombinedOutput() + if err != nil { + log.Printf("Command 'passwd %s' failed: %v\n%s", strings.Join(args, " "), err, output) } return err } diff --git a/test b/test index 38279e1..e6c7b4d 100755 --- a/test +++ b/test @@ -23,6 +23,7 @@ declare -a TESTPKGS=( datasource/metadata/cloudsigma datasource/metadata/digitalocean datasource/metadata/ec2 + datasource/metadata/openstack datasource/proc_cmdline datasource/url datasource/waagent