Merge pull request #15 from justinsb/master_pd

upup: Support for persistent disks, users & symlinks
This commit is contained in:
Mike Danese 2016-05-17 09:32:56 -07:00
commit 3703ecedc8
22 changed files with 685 additions and 52 deletions

View File

@ -2,7 +2,8 @@ gocode: godeps
go install k8s.io/kube-deploy/upup/cmd/...
godeps:
glide install
# I think strip-vendor is the workaround for 25572
glide install --strip-vendor --strip-vcs
tar: gocode
rm -rf .build/tar
@ -26,13 +27,14 @@ push: tar
scp .build/nodeup.tar.gz ${TARGET}:/tmp/
ssh ${TARGET} sudo tar zxf /tmp/nodeup.tar.gz -C /var/cache/kubernetes-install
push-dry: push
# GCE:
# ssh ${TARGET} sudo SKIP_PACKAGE_UPDATE=1 /var/cache/kubernetes-install/nodeup/root/nodeup --conf=metadata://gce/config --dryrun --v=8 --model=/var/cache/kubernetes-install/nodeup/root/model
push-gce-dry: push
ssh ${TARGET} sudo SKIP_PACKAGE_UPDATE=1 /var/cache/kubernetes-install/nodeup/root/nodeup --conf=metadata://gce/config --dryrun --v=8 --model=/var/cache/kubernetes-install/nodeup/root/model
push-aws-dry: push
ssh ${TARGET} sudo SKIP_PACKAGE_UPDATE=1 /var/cache/kubernetes-install/nodeup/root/nodeup --conf=/var/cache/kubernetes-install/kube_env.yaml --dryrun --v=8 --model=/var/cache/kubernetes-install/nodeup/root/model
push-run: push
# GCE:
# #ssh ${TARGET} sudo SKIP_PACKAGE_UPDATE=1 /var/cache/kubernetes-install/nodeup/root/nodeup --conf=metadata://gce/config --v=8 --model=/var/cache/kubernetes-install/nodeup/root/model
# AWS:
push-gce-run: push
ssh ${TARGET} sudo SKIP_PACKAGE_UPDATE=1 /var/cache/kubernetes-install/nodeup/root/nodeup --conf=metadata://gce/config --v=8 --model=/var/cache/kubernetes-install/nodeup/root/model
push-aws-run: push
ssh ${TARGET} sudo SKIP_PACKAGE_UPDATE=1 /var/cache/kubernetes-install/nodeup/root/nodeup --conf=/var/cache/kubernetes-install/kube_env.yaml --v=8 --model=/var/cache/kubernetes-install/nodeup/root/model

7
upup/glide.lock generated
View File

@ -1,5 +1,5 @@
hash: 9864d4fe0b94042cab08d2e80910638776d3d1e655b5a3cb9006731ae0306761
updated: 2016-05-09T11:48:29.232191357-04:00
updated: 2016-05-13T09:50:25.860201305-04:00
imports:
- name: github.com/aws/aws-sdk-go
version: d85fa529a99a833067e11c0a838b9db7a5d5ea71
@ -87,4 +87,9 @@ imports:
- internal
- name: google.golang.org/grpc
version: 9604a2bb7dd81d87c2873a9580258465f3c311c8
- name: k8s.io/kubernetes
version: bb3f5b1768f3bf6c81914c5bb1d7e846561fdc31
subpackages:
- pkg/util/exec
- pkg/util/mount
devImports: []

View File

@ -0,0 +1,5 @@
# TODO: Should we set a consistent UID in case we remount?
shell: /sbin/nologin
home: /var/etcd

View File

@ -0,0 +1,2 @@
device: /dev/xvdb
mountpoint: /mnt/master-pd

View File

@ -0,0 +1,2 @@
device: /dev/disk/by-id/google-master-pd
mountpoint: /mnt/master-pd

View File

@ -0,0 +1,4 @@
owner: etcd
group: etcd
directory: true
mode: "0700"

View File

@ -0,0 +1 @@
symlink: /mnt/master-pd/srv/kubernetes

View File

@ -0,0 +1 @@
symlink: /mnt/master-pd/srv/sshproxy

View File

@ -0,0 +1 @@
symlink: /mnt/master-pd/var/etcd

View File

@ -44,9 +44,9 @@ func (e *InstanceVolumeAttachment) Find(c *fi.Context) (*InstanceVolumeAttachmen
}
actual := &InstanceVolumeAttachment{
Device: bdm.DeviceName,
Device: bdm.DeviceName,
Instance: &Instance{ID: instance.InstanceId},
Volume: &EBSVolume{ID: bdm.Ebs.VolumeId},
Volume: &EBSVolume{ID: bdm.Ebs.VolumeId},
}
glog.V(2).Infof("found matching InstanceVolumeAttachmen")

View File

@ -7,6 +7,7 @@ import (
"os"
"path"
"strconv"
"syscall"
)
func WriteFile(destPath string, contents Resource, fileMode os.FileMode, dirMode os.FileMode) error {
@ -52,11 +53,11 @@ func writeFileContents(destPath string, src Resource, fileMode os.FileMode) erro
func EnsureFileMode(destPath string, fileMode os.FileMode) (bool, error) {
changed := false
stat, err := os.Stat(destPath)
stat, err := os.Lstat(destPath)
if err != nil {
return changed, fmt.Errorf("error getting file mode for %q: %v", destPath, err)
}
if stat.Mode() == fileMode {
if (stat.Mode() & os.ModePerm) == fileMode {
return changed, nil
}
glog.Infof("Changing file mode for %q to %s", destPath, fileMode)
@ -69,6 +70,37 @@ func EnsureFileMode(destPath string, fileMode os.FileMode) (bool, error) {
return changed, nil
}
func EnsureFileOwner(destPath string, owner string, groupName string) (bool, error) {
changed := false
stat, err := os.Lstat(destPath)
if err != nil {
return changed, fmt.Errorf("error getting file stat for %q: %v", destPath, err)
}
user, err := LookupUser(owner) //user.Lookup(owner)
if err != nil {
return changed, fmt.Errorf("error looking up user %q: %v", owner, err)
}
group, err := LookupGroup(groupName)
if err != nil {
return changed, fmt.Errorf("error looking up group %q: %v", groupName, err)
}
if int(stat.Sys().(*syscall.Stat_t).Uid) == user.Uid && int(stat.Sys().(*syscall.Stat_t).Gid) == group.Gid {
return changed, nil
}
glog.Infof("Changing file owner/group for %q to %s:%s", destPath, owner, group)
err = os.Lchown(destPath, user.Uid, group.Gid)
if err != nil {
return changed, fmt.Errorf("error setting file owner/group for %q: %v", destPath, err)
}
changed = true
return changed, nil
}
func fileHasHash(f string, expected string) (bool, error) {
hashAlgorithm, err := determineHashAlgorithm(expected)
if err != nil {

View File

@ -79,6 +79,22 @@ func (t *TreeWalker) walkDirectory(parent *TreeWalkItem) error {
glog.V(4).Infof("visit %q", i.Path)
hasMeta := false
{
metaPath := i.Path + ".meta"
metaBytes, err := ioutil.ReadFile(metaPath)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("error reading file %q: %v", metaPath, err)
}
metaBytes = nil
}
if metaBytes != nil {
hasMeta = true
i.Meta = string(metaBytes)
}
}
if f.IsDir() {
if IsTag(fileName) {
// Only descend into the tag directory if we have the tag
@ -106,7 +122,13 @@ func (t *TreeWalker) walkDirectory(parent *TreeWalkItem) error {
if err != nil {
return err
}
continue
// So that we can manage directories, we do not ignore directories which have a .meta file
if hasMeta {
glog.V(4).Infof("Found .meta file for directory %q; will process", i.Path)
} else {
continue
}
}
if strings.HasSuffix(fileName, ".meta") {
@ -120,24 +142,12 @@ func (t *TreeWalker) walkDirectory(parent *TreeWalkItem) error {
continue
}
{
metaPath := i.Path + ".meta"
metaBytes, err := ioutil.ReadFile(metaPath)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("error reading file %q: %v", metaPath, err)
}
metaBytes = nil
}
if metaBytes != nil {
i.Meta = string(metaBytes)
}
}
var handler Handler
if i.Context != "" {
handler = t.Contexts[i.Context]
} else {
// TODO: Just remove extensions.... we barely use them!
// (or remove default handler and replace with lots of small files?)
extension := path.Ext(fileName)
handler = t.Extensions[extension]
if handler == nil {

View File

@ -122,6 +122,10 @@ func (t *CloudInitTarget) WriteFile(destPath string, contents fi.Resource, fileM
return nil
}
func (t *CloudInitTarget) Chown(path string, user, group string) {
t.AddCommand(Always, "chown", user+":"+group, path)
}
func stringSlicesEquals(l, r []string) bool {
if len(l) != len(r) {
return false

View File

@ -9,6 +9,7 @@ import (
"k8s.io/kube-deploy/upup/pkg/fi"
"k8s.io/kube-deploy/upup/pkg/fi/loader"
"k8s.io/kube-deploy/upup/pkg/fi/nodeup/nodetasks"
"os"
"strings"
"text/template"
)
@ -75,9 +76,11 @@ func (l *Loader) Build(baseDir string) (map[string]fi.Task, error) {
DefaultHandler: ignoreHandler,
Contexts: map[string]loader.Handler{
"options": l.optionsLoader.HandleOptions,
"files": ignoreHandler,
"disks": ignoreHandler,
"packages": ignoreHandler,
"services": ignoreHandler,
"files": ignoreHandler,
"users": ignoreHandler,
},
Tags: tags,
}
@ -99,9 +102,11 @@ func (l *Loader) Build(baseDir string) (map[string]fi.Task, error) {
DefaultHandler: l.handleFile,
Contexts: map[string]loader.Handler{
"options": ignoreHandler,
"files": l.handleFile,
"disks": l.newTaskHandler("disk/", nodetasks.NewMountDiskTask),
"packages": l.newTaskHandler("package/", nodetasks.NewPackage),
"services": l.newTaskHandler("service/", nodetasks.NewService),
"files": l.handleFile,
"users": l.newTaskHandler("user/", nodetasks.NewUserTask),
},
Tags: tags,
}
@ -143,7 +148,9 @@ func (r *Loader) newTaskHandler(prefix string, builder TaskBuilder) loader.Handl
}
func (r *Loader) handleFile(i *loader.TreeWalkItem) error {
var task fi.Task
var task *nodetasks.File
defaultFileType := nodetasks.FileType_File
var err error
if strings.HasSuffix(i.RelativePath, ".template") {
contents, err := i.ReadString()
@ -185,7 +192,21 @@ func (r *Loader) handleFile(i *loader.TreeWalkItem) error {
task, err = nodetasks.NewFileTask(i.Name, asset, destPath, i.Meta)
} else {
task, err = nodetasks.NewFileTask(i.Name, fi.NewFileResource(i.Path), "/"+i.RelativePath, i.Meta)
stat, err := os.Stat(i.Path)
if err != nil {
return fmt.Errorf("error doing stat on %q: %v", i.Path, err)
}
var contents fi.Resource
if stat.IsDir() {
defaultFileType = nodetasks.FileType_Directory
} else {
contents = fi.NewFileResource(i.Path)
}
task, err = nodetasks.NewFileTask(i.Name, contents, "/"+i.RelativePath, i.Meta)
}
if task.Type == "" {
task.Type = defaultFileType
}
if err != nil {

View File

@ -1,17 +1,24 @@
package nodetasks
import (
"encoding/json"
"fmt"
"github.com/golang/glog"
"k8s.io/kube-deploy/upup/pkg/fi"
"k8s.io/kube-deploy/upup/pkg/fi/nodeup/cloudinit"
"k8s.io/kube-deploy/upup/pkg/fi/nodeup/local"
"k8s.io/kube-deploy/upup/pkg/fi/utils"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
)
const FileType_Symlink = "symlink"
const FileType_Directory = "directory"
const FileType_File = "file"
type File struct {
Path string
Contents fi.Resource
@ -20,9 +27,15 @@ type File struct {
IfNotExists bool `json:"ifNotExists"`
OnChangeExecute []string `json:"onChangeExecute,omitempty"`
Symlink *string `json:"symlink,omitempty"`
Owner *string `json:"owner,omitempty"`
Group *string `json:"group,omitempty"`
Type string `json:"type"`
}
var _ fi.Task = &File{}
var _ fi.HasDependencies = &File{}
func NewFileTask(name string, src fi.Resource, destPath string, meta string) (*File, error) {
f := &File{
@ -32,21 +45,43 @@ func NewFileTask(name string, src fi.Resource, destPath string, meta string) (*F
}
if meta != "" {
err := json.Unmarshal([]byte(meta), f)
err := utils.YamlUnmarshal([]byte(meta), f)
if err != nil {
return nil, fmt.Errorf("error parsing meta for file %q: %v", name, err)
}
}
if f.Symlink != nil && f.Type == "" {
f.Type = FileType_Symlink
}
return f, nil
}
func (f *File) GetDependencies(tasks map[string]fi.Task) []string {
var deps []string
if f.Owner != nil {
deps = append(deps, "user/"+*f.Owner)
}
// Depend on disk mounts
// For simplicity, we just depend on _all_ disk mounts
// We could check the mountpath, but that feels excessive...
for k, v := range tasks {
if _, ok := v.(*MountDiskTask); ok {
deps = append(deps, k)
}
}
return deps
}
func (f *File) String() string {
return fmt.Sprintf("File: %q", f.Path)
}
func findFile(p string) (*File, error) {
stat, err := os.Stat(p)
stat, err := os.Lstat(p)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
@ -55,8 +90,44 @@ func findFile(p string) (*File, error) {
actual := &File{}
actual.Path = p
actual.Mode = fi.String(fi.FileModeToString(stat.Mode()))
actual.Contents = fi.NewFileResource(p)
actual.Mode = fi.String(fi.FileModeToString(stat.Mode() & os.ModePerm))
uid := int(stat.Sys().(*syscall.Stat_t).Uid)
owner, err := fi.LookupUserById(uid)
if err != nil {
return nil, err
}
if owner != nil {
actual.Owner = fi.String(owner.Name)
} else {
actual.Owner = fi.String(strconv.Itoa(uid))
}
gid := int(stat.Sys().(*syscall.Stat_t).Gid)
group, err := fi.LookupGroupById(gid)
if err != nil {
return nil, err
}
if group != nil {
actual.Group = fi.String(group.Name)
} else {
actual.Group = fi.String(strconv.Itoa(gid))
}
if (stat.Mode() & os.ModeSymlink) != 0 {
target, err := os.Readlink(p)
if err != nil {
return nil, fmt.Errorf("error reading symlink target: %v", err)
}
actual.Type = FileType_Symlink
actual.Symlink = fi.String(target)
} else if (stat.Mode() & os.ModeDir) != 0 {
actual.Type = FileType_Directory
} else {
actual.Type = FileType_File
actual.Contents = fi.NewFileResource(p)
}
return actual, nil
}
@ -72,6 +143,10 @@ func (e *File) Find(c *fi.Context) (*File, error) {
// To avoid spurious changes
actual.IfNotExists = e.IfNotExists
if e.IfNotExists {
actual.Contents = e.Contents
}
actual.OnChangeExecute = e.OnChangeExecute
return actual, nil
}
@ -88,7 +163,7 @@ func (_ *File) RenderLocal(t *local.LocalTarget, a, e, changes *File) error {
dirMode := os.FileMode(0755)
fileMode, err := fi.ParseFileMode(fi.StringValue(e.Mode), 0644)
if err != nil {
return fmt.Errorf("invalid file mode for %q: %q", e.Path, e.Mode)
return fmt.Errorf("invalid file mode for %q: %q", e.Path, fi.StringValue(e.Mode))
}
if a != nil {
@ -99,20 +174,59 @@ func (_ *File) RenderLocal(t *local.LocalTarget, a, e, changes *File) error {
}
changed := false
if changes.Contents != nil {
err = fi.WriteFile(e.Path, e.Contents, fileMode, dirMode)
if err != nil {
return fmt.Errorf("error copying file %q: %v", e.Path, err)
if e.Type == FileType_Symlink {
if changes.Symlink != nil {
// This will currently fail if the target already exists.
// That's probably a good thing for now ... it is hard to know what to do here!
glog.Infof("Creating symlink %q -> %q", e.Path, *changes.Symlink)
err := os.Symlink(*changes.Symlink, e.Path)
if err != nil {
return fmt.Errorf("error creating symlink %q -> %q: %v", e.Path, *changes.Symlink, err)
}
changed = true
}
changed = true
} else if changes.Mode != nil {
} else if e.Type == FileType_Directory {
if a == nil {
parent := filepath.Dir(strings.TrimSuffix(e.Path, "/"))
err := os.MkdirAll(parent, dirMode)
if err != nil {
return fmt.Errorf("error creating parent directories %q: %v", parent, err)
}
err = os.MkdirAll(e.Path, fileMode)
if err != nil {
return fmt.Errorf("error creating directory %q: %v", e.Path, err)
}
changed = true
}
} else if e.Type == FileType_File {
if changes.Contents != nil {
err = fi.WriteFile(e.Path, e.Contents, fileMode, dirMode)
if err != nil {
return fmt.Errorf("error copying file %q: %v", e.Path, err)
}
changed = true
}
} else {
return fmt.Errorf("File type=%q not valid/supported", e.Type)
}
if changes.Mode != nil {
modeChanged, err := fi.EnsureFileMode(e.Path, fileMode)
if err != nil {
return fmt.Errorf("error changing file mode %q: %v", e.Path, err)
return fmt.Errorf("error changing mode on %q: %v", e.Path, err)
}
changed = changed || modeChanged
}
if changes.Owner != nil || changes.Group != nil {
ownerChanged, err := fi.EnsureFileOwner(e.Path, fi.StringValue(e.Owner), fi.StringValue(e.Group))
if err != nil {
return fmt.Errorf("error changing owner/group on %q: %v", e.Path, err)
}
changed = changed || ownerChanged
}
if changed && e.OnChangeExecute != nil {
args := e.OnChangeExecute
human := strings.Join(args, " ")
@ -136,9 +250,23 @@ func (_ *File) RenderCloudInit(t *cloudinit.CloudInitTarget, a, e, changes *File
return fmt.Errorf("invalid file mode for %q: %q", e.Path, e.Mode)
}
err = t.WriteFile(e.Path, e.Contents, fileMode, dirMode)
if err != nil {
return err
if e.Type == FileType_Symlink {
t.AddCommand(cloudinit.Always, "ln", "-s", fi.StringValue(e.Symlink), e.Path)
} else if e.Type == FileType_Directory {
parent := filepath.Dir(strings.TrimSuffix(e.Path, "/"))
t.AddCommand(cloudinit.Once, "mkdir", "-p", "-m", fi.FileModeToString(dirMode), parent)
t.AddCommand(cloudinit.Once, "mkdir", "-m", fi.FileModeToString(dirMode), e.Path)
} else if e.Type == FileType_File {
err = t.WriteFile(e.Path, e.Contents, fileMode, dirMode)
if err != nil {
return err
}
} else {
return fmt.Errorf("File type=%q not valid/supported", e.Type)
}
if e.Owner != nil || e.Group != nil {
t.Chown(e.Path, fi.StringValue(e.Owner), fi.StringValue(e.Group))
}
if e.OnChangeExecute != nil {

View File

@ -0,0 +1,136 @@
package nodetasks
import (
"fmt"
"github.com/golang/glog"
"k8s.io/kube-deploy/upup/pkg/fi"
"k8s.io/kube-deploy/upup/pkg/fi/nodeup/cloudinit"
"k8s.io/kube-deploy/upup/pkg/fi/nodeup/local"
"k8s.io/kube-deploy/upup/pkg/fi/utils"
"k8s.io/kubernetes/pkg/util/exec"
"k8s.io/kubernetes/pkg/util/mount"
"os"
"path/filepath"
"time"
)
// MountDiskTask is responsible for mounting a device on a mountpoint
// It will wait for the device to show up, safe_format_and_mount it,
// and then mount it.
type MountDiskTask struct {
Name string
Device string `json:"device"`
Mountpoint string `json:"mountpoint"`
}
var _ fi.Task = &MountDiskTask{}
func (s *MountDiskTask) String() string {
return fmt.Sprintf("Disk: %s", s.Name)
}
func NewMountDiskTask(name string, contents string, meta string) (fi.Task, error) {
s := &MountDiskTask{Name: name}
err := utils.YamlUnmarshal([]byte(contents), s)
if err != nil {
return nil, fmt.Errorf("error parsing json for disk %q: %v", name, err)
}
return s, nil
}
func (e *MountDiskTask) Find(c *fi.Context) (*MountDiskTask, error) {
mounter := mount.New()
mps, err := mounter.List()
if err != nil {
return nil, fmt.Errorf("error finding existing mounts: %v", err)
}
// If device is a symlink, it will show up by its final name
targetDevice, err := filepath.EvalSymlinks(e.Device)
if err != nil {
return nil, fmt.Errorf("error resolving device symlinks for %q: %v", e.Device, err)
}
for i := range mps {
mp := &mps[i]
if mp.Device == targetDevice {
actual := &MountDiskTask{
Name: e.Name,
Mountpoint: mp.Path,
Device: e.Device, // Use our alias, to keep change detection happy
}
return actual, nil
}
}
return nil, nil
}
func (e *MountDiskTask) Run(c *fi.Context) error {
return fi.DefaultDeltaRunMethod(e, c)
}
func (s *MountDiskTask) CheckChanges(a, e, changes *MountDiskTask) error {
return nil
}
func (_ *MountDiskTask) RenderLocal(t *local.LocalTarget, a, e, changes *MountDiskTask) error {
dirMode := os.FileMode(0755)
// Create the mountpoint
err := os.MkdirAll(e.Mountpoint, dirMode)
if err != nil {
return fmt.Errorf("error creating mountpoint %q: %v", e.Mountpoint, err)
}
// Wait for the device to show up
for {
_, err := os.Stat(e.Device)
if err == nil {
break
}
if !os.IsNotExist(err) {
return fmt.Errorf("error checking for device %q: %v", e.Device, err)
}
glog.Infof("Waiting for device %q to be attached", e.Device)
time.Sleep(1 * time.Second)
}
glog.Infof("Found device %q", e.Device)
// Mount the device
if changes.Mountpoint != "" {
glog.Infof("Mounting device %q on %q", e.Device, e.Mountpoint)
mounter := &mount.SafeFormatAndMount{Interface: mount.New(), Runner: exec.New()}
fstype := ""
options := []string{}
err := mounter.FormatAndMount(e.Device, e.Mountpoint, fstype, options)
if err != nil {
return fmt.Errorf("error formatting and mounting disk %q on %q: %v", e.Device, e.Mountpoint, err)
}
}
// TODO: Should we add to /etc/fstab?
// Mount the master PD as early as possible
// echo "/dev/xvdb /mnt/master-pd ext4 noatime 0 0" >> /etc/fstab
return nil
}
func (_ *MountDiskTask) RenderCloudInit(t *cloudinit.CloudInitTarget, a, e, changes *MountDiskTask) error {
// TODO: Run safe_format_and_mount
// Download on aws (or bake into image)
// # TODO: Where to get safe_format_and_mount?
//mkdir -p /usr/share/google
//cd /usr/share/google
//download-or-bust "dc96f40fdc9a0815f099a51738587ef5a976f1da" https://raw.githubusercontent.com/GoogleCloudPlatform/compute-image-packages/82b75f314528b90485d5239ab5d5495cc22d775f/google-startup-scripts/usr/share/google/safe_format_and_mount
//chmod +x safe_format_and_mount
return fmt.Errorf("Disk::RenderCloudInit not implemented")
}

View File

@ -36,12 +36,13 @@ func (p *Service) GetDependencies(tasks map[string]fi.Task) []string {
// We assume that services depend on basically everything
typeName := utils.BuildTypeName(reflect.TypeOf(v))
switch typeName {
case "*CopyAssetTask", "*File", "*Package", "*Sysctl", "*UpdatePackages":
case "*CopyAssetTask", "*File", "*Package", "*Sysctl", "*UpdatePackages", "*User", "*Disk":
deps = append(deps, k)
case "*Service":
// ignore
default:
glog.Fatalf("Unhandled type name: %q", typeName)
glog.Warningf("Unhandled type name in Service::GetDependencies: %q", typeName)
deps = append(deps, k)
}
}
return deps

View File

@ -0,0 +1,117 @@
package nodetasks
import (
"fmt"
"github.com/golang/glog"
"k8s.io/kube-deploy/upup/pkg/fi"
"k8s.io/kube-deploy/upup/pkg/fi/nodeup/cloudinit"
"k8s.io/kube-deploy/upup/pkg/fi/nodeup/local"
"k8s.io/kube-deploy/upup/pkg/fi/utils"
"os/exec"
)
// UserTask is responsible for creating a user, by calling useradd
type UserTask struct {
Name string
Shell string `json:"shell"`
Home string `json:"home"`
}
var _ fi.Task = &UserTask{}
func (e *UserTask) String() string {
return fmt.Sprintf("User: %s", e.Name)
}
func NewUserTask(name string, contents string, meta string) (fi.Task, error) {
s := &UserTask{Name: name}
err := utils.YamlUnmarshal([]byte(contents), s)
if err != nil {
return nil, fmt.Errorf("error parsing json for service %q: %v", name, err)
}
return s, nil
}
func (e *UserTask) Find(c *fi.Context) (*UserTask, error) {
info, err := fi.LookupUser(e.Name)
if err != nil {
return nil, err
}
if info == nil {
return nil, nil
}
actual := &UserTask{
Name: e.Name,
Shell: info.Shell,
Home: info.Home,
}
return actual, nil
}
func (e *UserTask) Run(c *fi.Context) error {
return fi.DefaultDeltaRunMethod(e, c)
}
func (_ *UserTask) CheckChanges(a, e, changes *UserTask) error {
return nil
}
func buildUseraddArgs(e *UserTask) []string {
var args []string
if e.Shell != "" {
args = append(args, "-s", e.Shell)
}
if e.Home != "" {
args = append(args, "-d", e.Home)
}
args = append(args, e.Name)
return args
}
func (_ *UserTask) RenderLocal(t *local.LocalTarget, a, e, changes *UserTask) error {
if a == nil {
args := buildUseraddArgs(e)
glog.Infof("Creating user %q", e.Name)
cmd := exec.Command("useradd", args...)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error creating user: %v\nOutput: %s", err, output)
}
} else {
var args []string
if changes.Shell != "" {
args = append(args, "-s", e.Shell)
}
if changes.Home != "" {
args = append(args, "-d", e.Home)
}
if len(args) != 0 {
args = append(args, e.Name)
glog.Infof("Reconfiguring user %q", e.Name)
cmd := exec.Command("usermod", args...)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error reconfiguring user: %v\nOutput: %s", err, output)
}
}
}
return nil
}
func (_ *UserTask) RenderCloudInit(t *cloudinit.CloudInitTarget, a, e, changes *UserTask) error {
args := buildUseraddArgs(e)
cmd := []string{"useradd"}
cmd = append(cmd, args...)
glog.Infof("Creating user %q", e.Name)
t.AddCommand(cloudinit.Once, cmd...)
return nil
}

View File

@ -32,11 +32,18 @@ func TopologicalSort(tasks map[string]Task) [][]string {
}
edges[k] = dependencyKeys
glog.V(4).Infof("%s : %v", k, dependencyKeys)
}
glog.V(4).Infof("Dependencies:")
for k, v := range edges {
glog.V(4).Infof("\t%s:\t%v", k, v)
}
ordered := toposort(edges)
glog.V(1).Infof("toposorted as %v", ordered)
glog.V(1).Infof("toposorted as:")
for i, stage := range ordered {
glog.V(1).Infof("\t%d\t%v", i, stage)
}
return ordered
}

152
upup/pkg/fi/users.go Normal file
View File

@ -0,0 +1,152 @@
package fi
import (
"fmt"
"github.com/golang/glog"
"io/ioutil"
"strconv"
"strings"
)
// This file parses /etc/passwd and /etc/group to get information about users & groups
// Go has built-in user functionality, and group functionality is merged but not yet released
// TODO: Replace this file with e.g. user.LookupGroup once 42f07ff2679d38a03522db3ccd488f4cc230c8c2 lands in go 1.7
type User struct {
Name string
Uid int
Gid int
Comment string
Home string
Shell string
}
func parseUsers() (map[string]*User, error) {
users := make(map[string]*User)
path := "/etc/passwd"
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading user file %q", path)
}
for _, line := range strings.Split(string(b), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
tokens := strings.Split(line, ":")
if len(tokens) < 7 {
glog.Warning("Ignoring malformed /etc/passwd line (too few tokens): %q", line)
continue
}
uid, err := strconv.Atoi(tokens[2])
if err != nil {
glog.Warning("Ignoring malformed /etc/passwd line (bad uid): %q", line)
continue
}
gid, err := strconv.Atoi(tokens[3])
if err != nil {
glog.Warning("Ignoring malformed /etc/passwd line (bad gid): %q", line)
continue
}
u := &User{
Name: tokens[0],
// Password
Uid: uid,
Gid: gid,
Comment: tokens[4],
Home: tokens[5],
Shell: tokens[6],
}
users[u.Name] = u
}
return users, nil
}
func LookupUser(name string) (*User, error) {
users, err := parseUsers()
if err != nil {
return nil, fmt.Errorf("error reading users: %v", err)
}
return users[name], nil
}
func LookupUserById(uid int) (*User, error) {
users, err := parseUsers()
if err != nil {
return nil, fmt.Errorf("error reading users: %v", err)
}
for _, v := range users {
if v.Uid == uid {
return v, nil
}
}
return nil, nil
}
type Group struct {
Name string
Gid int
//Members []string
}
func parseGroups() (map[string]*Group, error) {
groups := make(map[string]*Group)
path := "/etc/group"
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading group file %q", path)
}
for _, line := range strings.Split(string(b), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
tokens := strings.Split(line, ":")
if len(tokens) < 4 {
glog.Warning("Ignoring malformed /etc/group line (too few tokens): %q", line)
continue
}
gid, err := strconv.Atoi(tokens[2])
if err != nil {
glog.Warning("Ignoring malformed /etc/group line (bad gid): %q", line)
continue
}
g := &Group{
Name: tokens[0],
// Password: tokens[1]
Gid: gid,
// Members: strings.Split(tokens[3], ",")
}
groups[g.Name] = g
}
return groups, nil
}
func LookupGroup(name string) (*Group, error) {
groups, err := parseGroups()
if err != nil {
return nil, fmt.Errorf("error reading groups: %v", err)
}
return groups[name], nil
}
func LookupGroupById(gid int) (*Group, error) {
users, err := parseGroups()
if err != nil {
return nil, fmt.Errorf("error reading groups: %v", err)
}
for _, v := range users {
if v.Gid == gid {
return v, nil
}
}
return nil, nil
}