Mirantis/virtlet

View on GitHub
pkg/libvirttools/cloudinit.go

Summary

Maintainability
A
0 mins
Test Coverage
/*
Copyright 2017 Mirantis

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 libvirttools

import (
    "bytes"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net"
    "os"
    "path"
    "path/filepath"
    "strconv"
    "strings"

    cnitypes "github.com/containernetworking/cni/pkg/types"
    cnicurrent "github.com/containernetworking/cni/pkg/types/current"
    "github.com/ghodss/yaml"
    "github.com/golang/glog"
    "github.com/kballard/go-shellquote"
    libvirtxml "github.com/libvirt/libvirt-go-xml"

    "github.com/Mirantis/virtlet/pkg/flexvolume"
    "github.com/Mirantis/virtlet/pkg/fs"
    "github.com/Mirantis/virtlet/pkg/metadata/types"
    "github.com/Mirantis/virtlet/pkg/network"
    "github.com/Mirantis/virtlet/pkg/utils"
)

const (
    envFileLocation     = "/etc/cloud/environment"
    symlinkFileLocation = "/etc/cloud/symlink-devs.sh"
    mountFileLocation   = "/etc/cloud/mount-volumes.sh"
    mountScriptSubst    = "@virtlet-mount-script@"
    cloudInitPerBootDir = "/var/lib/cloud/scripts/per-boot"
)

// Note that in the templates below, we don't use shellquote (shq) on
// SysfsPath, because it *must* be expanded by the shell to work
// (it contains '*')

var linkStartupScriptTemplate = utils.NewShellTemplate(
    "ln -s {{ shq .StartupScript }} /var/lib/cloud/scripts/per-boot/")
var linkBlockDeviceScriptTemplate = utils.NewShellTemplate(
    "ln -fs /dev/`ls {{ .SysfsPath }}` {{ shq .DevicePath }}")
var mountDevScriptTemplate = utils.NewShellTemplate(
    "if ! mountpoint {{ shq .ContainerPath }}; then " +
        "mkdir -p {{ shq .ContainerPath }} && " +
        "mount /dev/`ls {{ .SysfsPath }}`{{ .DevSuffix }} {{ .ContainerPath }}; " +
        "fi")
var mountFSScriptTemplate = utils.NewShellTemplate(
    "if ! mountpoint {{ shq .ContainerPath }}; then " +
        "mkdir -p {{ shq .ContainerPath }} && " +
        "mount -t 9p -o trans=virtio {{ shq .MountTag }} {{ shq .ContainerPath }}; " +
        "fi")

// CloudInitGenerator provides a common part for Cloud Init ISO drive preparation
// for NoCloud and ConfigDrive volume sources.
type CloudInitGenerator struct {
    config *types.VMConfig
    isoDir string
}

// NewCloudInitGenerator returns new CloudInitGenerator.
func NewCloudInitGenerator(config *types.VMConfig, isoDir string) *CloudInitGenerator {
    return &CloudInitGenerator{
        config: config,
        isoDir: isoDir,
    }
}

func (g *CloudInitGenerator) generateMetaData() ([]byte, error) {
    m := map[string]interface{}{
        "instance-id":    fmt.Sprintf("%s.%s", g.config.PodName, g.config.PodNamespace),
        "local-hostname": g.config.PodName,
    }

    // TODO: get rid of this if. Use descriptor for cloud-init image types.
    if g.config.ParsedAnnotations.CDImageType == types.CloudInitImageTypeConfigDrive {
        m["uuid"] = m["instance-id"]
        m["hostname"] = m["local-hostname"]
    }

    if len(g.config.ParsedAnnotations.SSHKeys) != 0 {
        var keys []string
        for _, key := range g.config.ParsedAnnotations.SSHKeys {
            keys = append(keys, key)
        }
        m["public-keys"] = keys
    }

    for k, v := range g.config.ParsedAnnotations.MetaData {
        m[k] = v
    }

    r, err := json.Marshal(m)
    if err != nil {
        return nil, fmt.Errorf("error marshaling meta-data: %v", err)
    }

    return r, nil
}

func (g *CloudInitGenerator) generateUserData(volumeMap diskPathMap) ([]byte, error) {
    symlinkScript := g.generateSymlinkScript(volumeMap)
    mounts, mountScript := g.generateMounts(volumeMap)

    if userDataScript := g.config.ParsedAnnotations.UserDataScript; userDataScript != "" {
        fullMountScript := ""
        switch {
        case mountScript != "" && symlinkScript != "":
            fullMountScript = symlinkScript + "\n" + mountScript
        case mountScript != "":
            fullMountScript = mountScript
        case symlinkScript != "":
            fullMountScript = symlinkScript
        }
        return []byte(strings.Replace(userDataScript, mountScriptSubst, fullMountScript, -1)), nil
    }

    userData := make(map[string]interface{})
    for k, v := range g.config.ParsedAnnotations.UserData {
        userData[k] = v
    }

    mounts = utils.Merge(userData["mounts"], mounts).([]interface{})
    if len(mounts) != 0 {
        userData["mounts"] = g.fixMounts(volumeMap, mounts)
    }

    writeFilesUpdater := newWriteFilesUpdater(g.config.Mounts)
    writeFilesUpdater.addSecrets()
    writeFilesUpdater.addConfigMapEntries()
    writeFilesUpdater.addFileLikeMounts()
    if symlinkScript != "" {
        writeFilesUpdater.addSymlinkScript(symlinkScript)
        userData["runcmd"] = utils.Merge(userData["runcmd"], []string{
            shellquote.Join(symlinkFileLocation),
            linkStartupScriptTemplate.MustExecuteToString(map[string]string{
                "StartupScript": symlinkFileLocation,
            }),
        })
    }
    if mountScript != "" {
        writeFilesUpdater.addMountScript(mountScript)
    }
    if envContent := g.generateEnvVarsContent(); envContent != "" {
        writeFilesUpdater.addEnvironmentFile(envContent)
    }
    writeFilesUpdater.updateUserData(userData)

    r := []byte{}
    if len(userData) != 0 {
        var err error
        r, err = yaml.Marshal(userData)
        if err != nil {
            return nil, fmt.Errorf("error marshalling user-data: %v", err)
        }
    }
    return []byte("#cloud-config\n" + string(r)), nil
}

func (g *CloudInitGenerator) generateNetworkConfiguration() ([]byte, error) {
    if g.config.ParsedAnnotations.ForceDHCPNetworkConfig || g.config.RootVolumeDevice() != nil {
        // Don't use cloud-init network config if asked not
        // to do so.
        // Also, we don't use network config with persistent
        // rootfs for now because with some cloud-init
        // implementations it's applied only once
        return nil, nil
    }
    // TODO: get rid of this switch. Use descriptor for cloud-init image types.
    switch g.config.ParsedAnnotations.CDImageType {
    case types.CloudInitImageTypeNoCloud:
        return g.generateNetworkConfigurationNoCloud()
    case types.CloudInitImageTypeConfigDrive:
        return g.generateNetworkConfigurationConfigDrive()
    }

    return nil, fmt.Errorf("unknown cloud-init config image type: %q", g.config.ParsedAnnotations.CDImageType)
}

func (g *CloudInitGenerator) generateNetworkConfigurationNoCloud() ([]byte, error) {
    if g.config.ContainerSideNetwork == nil {
        // This can only happen during integration tests
        // where a dummy sandbox is used
        return []byte("version: 1\n"), nil
    }
    cniResult := g.config.ContainerSideNetwork.Result

    var config []map[string]interface{}

    // physical interfaces
    for i, iface := range cniResult.Interfaces {
        if iface.Sandbox == "" {
            // skip host interfaces
            continue
        }
        subnets := g.getSubnetsForNthInterface(i, cniResult)
        mtu, err := mtuForMacAddress(iface.Mac, g.config.ContainerSideNetwork.Interfaces)
        if err != nil {
            return nil, err
        }
        interfaceConf := map[string]interface{}{
            "type":        "physical",
            "name":        iface.Name,
            "mac_address": iface.Mac,
            "subnets":     subnets,
            "mtu":         mtu,
        }
        config = append(config, interfaceConf)
    }

    // dns
    dnsData := getDNSData(cniResult.DNS)
    if dnsData != nil {
        config = append(config, dnsData...)
    }

    r, err := yaml.Marshal(map[string]interface{}{
        "config": config,
    })
    if err != nil {
        return nil, err
    }
    return []byte("version: 1\n" + string(r)), nil
}

func (g *CloudInitGenerator) getSubnetsForNthInterface(interfaceNo int, cniResult *cnicurrent.Result) []map[string]interface{} {
    var subnets []map[string]interface{}
    routes := append(cniResult.Routes[:0:0], cniResult.Routes...)
    gotDefault := false
    for _, ipConfig := range cniResult.IPs {
        if ipConfig.Interface == interfaceNo {
            subnet := map[string]interface{}{
                "type":    "static",
                "address": ipConfig.Address.IP.String(),
                "netmask": net.IP(ipConfig.Address.Mask).String(),
            }

            var subnetRoutes []map[string]interface{}
            // iterate on routes slice in reverse order because at
            // the end of loop found element will be removed from slice
            allRoutesLen := len(routes)
            for i := range routes {
                cniRoute := routes[allRoutesLen-1-i]
                var gw net.IP
                if cniRoute.GW != nil && ipConfig.Address.Contains(cniRoute.GW) {
                    gw = cniRoute.GW
                } else if cniRoute.GW == nil && !ipConfig.Gateway.IsUnspecified() {
                    gw = ipConfig.Gateway
                } else {
                    continue
                }
                if ones, _ := cniRoute.Dst.Mask.Size(); ones == 0 {
                    if gotDefault {
                        glog.Warning("cloud-init: got more than one default route, using only the first one")
                        continue
                    }
                    gotDefault = true
                }
                route := map[string]interface{}{
                    "network": cniRoute.Dst.IP.String(),
                    "netmask": net.IP(cniRoute.Dst.Mask).String(),
                    "gateway": gw.String(),
                }
                subnetRoutes = append(subnetRoutes, route)
                routes = append(routes[:allRoutesLen-1-i], routes[allRoutesLen-i:]...)
            }
            if subnetRoutes != nil {
                subnet["routes"] = subnetRoutes
            }

            subnets = append(subnets, subnet)
        }
    }

    // fallback to dhcp - should never happen, we always should have IPs
    if subnets == nil {
        subnets = append(subnets, map[string]interface{}{
            "type": "dhcp",
        })
    }

    return subnets
}

func getDNSData(cniDNS cnitypes.DNS) []map[string]interface{} {
    var dnsData []map[string]interface{}
    if cniDNS.Nameservers != nil {
        dnsData = append(dnsData, map[string]interface{}{
            "type":    "nameserver",
            "address": cniDNS.Nameservers,
        })
        if cniDNS.Search != nil {
            dnsData[0]["search"] = cniDNS.Search
        }
    }
    return dnsData
}

func (g *CloudInitGenerator) generateNetworkConfigurationConfigDrive() ([]byte, error) {
    if g.config.ContainerSideNetwork == nil {
        // This can only happen during integration tests
        // where a dummy sandbox is used
        return []byte("{}"), nil
    }
    cniResult := g.config.ContainerSideNetwork.Result

    config := make(map[string]interface{})

    // links
    var links []map[string]interface{}
    for _, iface := range cniResult.Interfaces {
        if iface.Sandbox == "" {
            // skip host interfaces
            continue
        }
        mtu, err := mtuForMacAddress(iface.Mac, g.config.ContainerSideNetwork.Interfaces)
        if err != nil {
            return nil, err
        }
        linkConf := map[string]interface{}{
            "type":                 "phy",
            "id":                   iface.Name,
            "ethernet_mac_address": iface.Mac,
            "mtu":                  mtu,
        }
        links = append(links, linkConf)
    }
    config["links"] = links

    var networks []map[string]interface{}
    for i, ipConfig := range cniResult.IPs {
        netConf := map[string]interface{}{
            "id": fmt.Sprintf("net-%d", i),
            // config from openstack have as network_id network uuid
            "network_id": fmt.Sprintf("net-%d", i),
            "type":       fmt.Sprintf("ipv%s", ipConfig.Version),
            "link":       cniResult.Interfaces[ipConfig.Interface].Name,
            "ip_address": ipConfig.Address.IP.String(),
            "netmask":    net.IP(ipConfig.Address.Mask).String(),
        }

        routes := routesForIP(ipConfig.Address, cniResult.Routes)
        if routes != nil {
            netConf["routes"] = routes
        }

        networks = append(networks, netConf)
    }
    config["networks"] = networks

    dnsData := getDNSData(cniResult.DNS)
    if dnsData != nil {
        config["services"] = dnsData
    }

    r, err := json.Marshal(config)
    if err != nil {
        return nil, fmt.Errorf("error marshaling network configuration: %v", err)
    }
    return r, nil
}

func routesForIP(sourceIP net.IPNet, allRoutes []*cnitypes.Route) []map[string]interface{} {
    var routes []map[string]interface{}

    // NOTE: at the moment on cni result level there is no distinction
    // for which interface particular route should be set,
    // so we are returning there all routes with gateway accessible
    // by particular source ip address.
    for _, route := range allRoutes {
        if sourceIP.Contains(route.GW) {
            routes = append(routes, map[string]interface{}{
                "network": route.Dst.IP.String(),
                "netmask": net.IP(route.Dst.Mask).String(),
                "gateway": route.GW.String(),
            })
        }
    }

    return routes
}

func mtuForMacAddress(mac string, ifaces []*network.InterfaceDescription) (uint16, error) {
    for _, iface := range ifaces {
        if iface.HardwareAddr.String() == strings.ToLower(mac) {
            return iface.MTU, nil
        }
    }
    return 0, fmt.Errorf("interface with mac address %q not found in ContainerSideNetwork", mac)
}

// IsoPath returns a full path to iso image with configuration for VM pod.
func (g *CloudInitGenerator) IsoPath() string {
    return filepath.Join(g.isoDir, fmt.Sprintf("config-%s.iso", g.config.DomainUUID))
}

// DiskDef returns a DomainDisk definition for Cloud Init ISO image to be included
// in VM pod libvirt domain definition.
func (g *CloudInitGenerator) DiskDef() *libvirtxml.DomainDisk {
    return &libvirtxml.DomainDisk{
        Device:   "cdrom",
        Driver:   &libvirtxml.DomainDiskDriver{Name: "qemu", Type: "raw"},
        Source:   &libvirtxml.DomainDiskSource{File: &libvirtxml.DomainDiskSourceFile{File: g.IsoPath()}},
        ReadOnly: &libvirtxml.DomainDiskReadOnly{},
    }
}

// GenerateImage collects metadata, userdata and network configuration and uses
// them to prepare an ISO image for NoCloud or ConfigDrive selecting the type
// using an info from pod annotations.
func (g *CloudInitGenerator) GenerateImage(volumeMap diskPathMap) error {
    tmpDir, err := ioutil.TempDir("", "config-")
    if err != nil {
        return fmt.Errorf("can't create temp dir for config image: %v", err)
    }
    defer os.RemoveAll(tmpDir)

    var metaData, userData, networkConfiguration []byte
    metaData, err = g.generateMetaData()
    if err == nil {
        userData, err = g.generateUserData(volumeMap)
    }
    if err == nil {
        networkConfiguration, err = g.generateNetworkConfiguration()
    }
    if err != nil {
        return err
    }

    var userDataLocation, metaDataLocation, networkConfigLocation string
    var volumeName string

    // TODO: get rid of this switch. Use descriptor for cloud-init image types.
    switch g.config.ParsedAnnotations.CDImageType {
    case types.CloudInitImageTypeNoCloud:
        userDataLocation = "user-data"
        metaDataLocation = "meta-data"
        networkConfigLocation = "network-config"
        volumeName = "cidata"
    case types.CloudInitImageTypeConfigDrive:
        userDataLocation = "openstack/latest/user_data"
        metaDataLocation = "openstack/latest/meta_data.json"
        networkConfigLocation = "openstack/latest/network_data.json"
        volumeName = "config-2"
    default:
        // that should newer happen, as imageType should be validated
        // already earlier
        return fmt.Errorf("unknown cloud-init config image type: %q", g.config.ParsedAnnotations.CDImageType)
    }

    fileMap := map[string][]byte{
        userDataLocation: userData,
        metaDataLocation: metaData,
    }
    if networkConfiguration != nil {
        fileMap[networkConfigLocation] = networkConfiguration
    }
    if err := fs.WriteFiles(tmpDir, fileMap); err != nil {
        return fmt.Errorf("can't write user-data: %v", err)
    }

    if err := os.MkdirAll(g.isoDir, 0777); err != nil {
        return fmt.Errorf("error making iso directory %q: %v", g.isoDir, err)
    }

    if err := fs.GenIsoImage(g.IsoPath(), volumeName, tmpDir); err != nil {
        if rmErr := os.Remove(g.IsoPath()); rmErr != nil {
            glog.Warningf("Error removing iso file %s: %v", g.IsoPath(), rmErr)
        }
        return fmt.Errorf("error generating iso image: %v", err)
    }

    return nil
}

func (g *CloudInitGenerator) generateEnvVarsContent() string {
    var buffer bytes.Buffer
    for _, entry := range g.config.Environment {
        buffer.WriteString(fmt.Sprintf("%s=%s\n", entry.Key, entry.Value))
    }

    return buffer.String()
}

func isRegularFile(path string) bool {
    fi, err := os.Stat(path)
    if err != nil {
        return false
    }
    return fi.Mode().IsRegular()
}

func (g *CloudInitGenerator) generateSymlinkScript(volumeMap diskPathMap) string {
    var symlinkLines []string
    for _, dev := range g.config.VolumeDevices {
        if dev.IsRoot() {
            // special case for the persistent rootfs
            continue
        }
        dpath, found := volumeMap[dev.UUID()]
        if !found {
            glog.Warningf("Couldn't determine the path for device %q inside the VM (target path inside the VM: %q)", dev.HostPath, dev.DevicePath)
            continue
        }
        line := linkBlockDeviceScriptTemplate.MustExecuteToString(map[string]string{
            "SysfsPath":  dpath.sysfsPath,
            "DevicePath": dev.DevicePath,
        })
        symlinkLines = append(symlinkLines, line)
    }
    return makeScript(symlinkLines)
}

func (g *CloudInitGenerator) fixMounts(volumeMap diskPathMap, mounts []interface{}) []interface{} {
    devMap := make(map[string]string)
    for _, dev := range g.config.VolumeDevices {
        if dev.IsRoot() {
            // special case for the persistent rootfs
            continue
        }
        dpath, found := volumeMap[dev.UUID()]
        if !found {
            glog.Warningf("Couldn't determine the path for device %q inside the VM (target path inside the VM: %q)", dev.HostPath, dev.DevicePath)
            continue
        }
        devMap[dev.DevicePath] = dpath.devPath
    }
    if len(devMap) == 0 {
        return mounts
    }

    var r []interface{}
    for _, item := range mounts {
        m, ok := item.([]interface{})
        if !ok || len(m) == 0 {
            r = append(r, item)
            continue
        }
        devPath, ok := m[0].(string)
        if !ok {
            r = append(r, item)
            continue
        }
        mapTo, found := devMap[devPath]
        if !found {
            r = append(r, item)
            continue
        }
        r = append(r, append([]interface{}{mapTo}, m[1:]...))
    }
    return r
}

func (g *CloudInitGenerator) generateMounts(volumeMap diskPathMap) ([]interface{}, string) {
    var r []interface{}
    var mountScriptLines []string
    for _, m := range g.config.Mounts {
        // Skip file based mounts (including secrets and config maps).
        if isRegularFile(m.HostPath) ||
            strings.Contains(m.HostPath, "kubernetes.io~secret") ||
            strings.Contains(m.HostPath, "kubernetes.io~configmap") {
            continue
        }

        mountInfo, mountScriptLine, err := generateFlexvolumeMounts(volumeMap, m)
        if err != nil {
            if !os.IsNotExist(err) {
                glog.Errorf("Can't mount directory %q to %q inside the VM: %v", m.HostPath, m.ContainerPath, err)
                continue
            }

            // Fs based volume
            mountInfo, mountScriptLine, err = generateFsBasedVolumeMounts(m)
            if err != nil {
                glog.Errorf("Can't mount directory %q to %q inside the VM: %v", m.HostPath, m.ContainerPath, err)
                continue
            }
        }

        r = append(r, mountInfo)
        mountScriptLines = append(mountScriptLines, mountScriptLine)
    }

    return r, makeScript(mountScriptLines)
}

func generateFlexvolumeMounts(volumeMap diskPathMap, mount types.VMMount) ([]interface{}, string, error) {
    uuid, part, err := flexvolume.GetFlexvolumeInfo(mount.HostPath)
    if err != nil {
        // If the error is NotExist, return the original error
        if os.IsNotExist(err) {
            return nil, "", err
        }
        err = fmt.Errorf("can't get flexvolume uuid: %v", err)
        return nil, "", err
    }
    dpath, found := volumeMap[uuid]
    if !found {
        err = fmt.Errorf("no device found for flexvolume uuid %q", uuid)
        return nil, "", err
    }
    if part < 0 {
        part = 1
    }
    devPath := dpath.devPath
    mountDevSuffix := ""
    if part != 0 {
        devPath += fmt.Sprintf("-part%d", part)
        mountDevSuffix += strconv.Itoa(part)
    }
    mountScriptLine := mountDevScriptTemplate.MustExecuteToString(map[string]string{
        "ContainerPath": mount.ContainerPath,
        "SysfsPath":     dpath.sysfsPath,
        "DevSuffix":     mountDevSuffix,
    })
    return []interface{}{devPath, mount.ContainerPath}, mountScriptLine, nil
}

func generateFsBasedVolumeMounts(mount types.VMMount) ([]interface{}, string, error) {
    mountTag := path.Base(mount.ContainerPath)
    fsMountScript := mountFSScriptTemplate.MustExecuteToString(map[string]string{
        "ContainerPath": mount.ContainerPath,
        "MountTag":      mountTag,
    })
    r := []interface{}{mountTag, mount.ContainerPath, "9p", "trans=virtio"}
    return r, fsMountScript, nil
}

type writeFilesUpdater struct {
    entries []interface{}
    mounts  []types.VMMount
}

func newWriteFilesUpdater(mounts []types.VMMount) *writeFilesUpdater {
    return &writeFilesUpdater{
        mounts: mounts,
    }
}

func (u *writeFilesUpdater) put(entry interface{}) {
    u.entries = append(u.entries, entry)
}

func (u *writeFilesUpdater) putPlainText(path string, content string, perms os.FileMode) {
    u.put(map[string]interface{}{
        "path":        path,
        "content":     content,
        "permissions": fmt.Sprintf("%#o", uint32(perms)),
    })
}

func (u *writeFilesUpdater) putBase64(path string, content []byte, perms os.FileMode) {
    encodedContent := base64.StdEncoding.EncodeToString(content)
    u.put(map[string]interface{}{
        "path":        path,
        "content":     encodedContent,
        "encoding":    "b64",
        "permissions": fmt.Sprintf("%#o", uint32(perms)),
    })
}

func (u *writeFilesUpdater) updateUserData(userData map[string]interface{}) {
    if len(u.entries) == 0 {
        return
    }

    writeFiles := utils.Merge(userData["write_files"], u.entries).([]interface{})
    if len(writeFiles) != 0 {
        userData["write_files"] = writeFiles
    }
}

func (u *writeFilesUpdater) addSecrets() {
    u.addFilesForVolumeType("secret")
}

func (u *writeFilesUpdater) addConfigMapEntries() {
    u.addFilesForVolumeType("configmap")
}

func (u *writeFilesUpdater) addFileLikeMounts() {
    for _, mount := range u.filterMounts(func(path string) bool {
        fi, err := os.Stat(path)
        switch {
        case err != nil:
            return false
        case fi.Mode().IsRegular():
            return true
        }
        return false
    }) {
        content, err := ioutil.ReadFile(mount.HostPath)
        if err != nil {
            glog.Warningf("Error during reading content of '%s' file: %v", mount.HostPath, err)
            continue
        }

        glog.V(3).Infof("Adding file '%s' as volume: %s", mount.HostPath, mount.ContainerPath)
        u.putBase64(mount.ContainerPath, content, 0644)
    }
}

func (u *writeFilesUpdater) addFilesForVolumeType(suffix string) {
    filter := "volumes/kubernetes.io~" + suffix + "/"
    for _, mount := range u.filterMounts(func(path string) bool {
        return strings.Contains(path, filter)
    }) {
        u.addFilesForMount(mount)
    }
}

func (u *writeFilesUpdater) addSymlinkScript(content string) {
    u.putPlainText(symlinkFileLocation, content, 0755)
}

func (u *writeFilesUpdater) addMountScript(content string) {
    u.putPlainText(mountFileLocation, content, 0755)
}

func (u *writeFilesUpdater) addEnvironmentFile(content string) {
    u.putPlainText(envFileLocation, content, 0644)
}

func (u *writeFilesUpdater) addFilesForMount(mount types.VMMount) []interface{} {
    var writeFiles []interface{}

    addFileContent := func(fullPath string) error {
        content, err := ioutil.ReadFile(fullPath)
        if err != nil {
            return err
        }
        stat, err := os.Stat(fullPath)
        if err != nil {
            return err
        }
        relativePath := fullPath[len(mount.HostPath)+1:]
        u.putBase64(path.Join(mount.ContainerPath, relativePath), content, stat.Mode())
        return nil
    }

    glog.V(3).Infof("Scanning %s for files", mount.HostPath)
    if err := scanDirectory(mount.HostPath, addFileContent); err != nil {
        glog.Errorf("Error while scanning directory %s: %v", mount.HostPath, err)
    }
    glog.V(3).Infof("Found %d entries", len(writeFiles))

    return writeFiles
}

func (u *writeFilesUpdater) filterMounts(filter func(string) bool) []types.VMMount {
    var r []types.VMMount
    for _, mount := range u.mounts {
        if filter(mount.HostPath) {
            r = append(r, mount)
        }
    }
    return r
}

func scanDirectory(dirPath string, callback func(string) error) error {
    entries, err := ioutil.ReadDir(dirPath)
    if err != nil {
        return err
    }

    for _, entry := range entries {
        fullPath := path.Join(dirPath, entry.Name())

        switch {
        case entry.Mode().IsDir():
            glog.V(3).Infof("Scanning directory: %s", entry.Name())
            if err := scanDirectory(fullPath, callback); err != nil {
                return err
            }
            glog.V(3).Infof("Leaving directory: %s", entry.Name())
        case entry.Mode().IsRegular():
            glog.V(3).Infof("Found regular file: %s", entry.Name())
            err := callback(fullPath)
            if err != nil {
                return err
            }
            continue
        case entry.Mode()&os.ModeSymlink != 0:
            glog.V(3).Infof("Found symlink: %s", entry.Name())
            fi, err := os.Stat(fullPath)
            switch {
            case err != nil:
                return err
            case fi.Mode().IsRegular():
                err = callback(fullPath)
                if err != nil {
                    return err
                }
            case fi.Mode().IsDir():
                glog.V(3).Info("... which points to directory, going deeper ...")

                // NOTE: this does not need to be protected against loops
                // because it's prepared by kubelet in safe manner (if it's not
                // it's bug on kubelet side
                if err := scanDirectory(fullPath, callback); err != nil {
                    return err
                }
                glog.V(3).Infof("... came back from symlink to directory: %s", entry.Name())
            default:
                glog.V(3).Info("... but it's pointing to something other than directory or regular file")
            }
        }
    }

    return nil
}

func makeScript(lines []string) string {
    if len(lines) != 0 {
        return fmt.Sprintf("#!/bin/sh\n%s\n", strings.Join(lines, "\n"))
    }
    return ""
}