github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/libvirttools/cloudinit.go (about)

     1  /*
     2  Copyright 2017 Mirantis
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package libvirttools
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/base64"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"net"
    26  	"os"
    27  	"path"
    28  	"path/filepath"
    29  	"strconv"
    30  	"strings"
    31  
    32  	cnitypes "github.com/containernetworking/cni/pkg/types"
    33  	cnicurrent "github.com/containernetworking/cni/pkg/types/current"
    34  	"github.com/ghodss/yaml"
    35  	"github.com/golang/glog"
    36  	"github.com/kballard/go-shellquote"
    37  	libvirtxml "github.com/libvirt/libvirt-go-xml"
    38  
    39  	"github.com/Mirantis/virtlet/pkg/flexvolume"
    40  	"github.com/Mirantis/virtlet/pkg/fs"
    41  	"github.com/Mirantis/virtlet/pkg/metadata/types"
    42  	"github.com/Mirantis/virtlet/pkg/network"
    43  	"github.com/Mirantis/virtlet/pkg/utils"
    44  )
    45  
    46  const (
    47  	envFileLocation     = "/etc/cloud/environment"
    48  	symlinkFileLocation = "/etc/cloud/symlink-devs.sh"
    49  	mountFileLocation   = "/etc/cloud/mount-volumes.sh"
    50  	mountScriptSubst    = "@virtlet-mount-script@"
    51  	cloudInitPerBootDir = "/var/lib/cloud/scripts/per-boot"
    52  )
    53  
    54  // Note that in the templates below, we don't use shellquote (shq) on
    55  // SysfsPath, because it *must* be expanded by the shell to work
    56  // (it contains '*')
    57  
    58  var linkStartupScriptTemplate = utils.NewShellTemplate(
    59  	"ln -s {{ shq .StartupScript }} /var/lib/cloud/scripts/per-boot/")
    60  var linkBlockDeviceScriptTemplate = utils.NewShellTemplate(
    61  	"ln -fs /dev/`ls {{ .SysfsPath }}` {{ shq .DevicePath }}")
    62  var mountDevScriptTemplate = utils.NewShellTemplate(
    63  	"if ! mountpoint {{ shq .ContainerPath }}; then " +
    64  		"mkdir -p {{ shq .ContainerPath }} && " +
    65  		"mount /dev/`ls {{ .SysfsPath }}`{{ .DevSuffix }} {{ .ContainerPath }}; " +
    66  		"fi")
    67  var mountFSScriptTemplate = utils.NewShellTemplate(
    68  	"if ! mountpoint {{ shq .ContainerPath }}; then " +
    69  		"mkdir -p {{ shq .ContainerPath }} && " +
    70  		"mount -t 9p -o trans=virtio {{ shq .MountTag }} {{ shq .ContainerPath }}; " +
    71  		"fi")
    72  
    73  // CloudInitGenerator provides a common part for Cloud Init ISO drive preparation
    74  // for NoCloud and ConfigDrive volume sources.
    75  type CloudInitGenerator struct {
    76  	config *types.VMConfig
    77  	isoDir string
    78  }
    79  
    80  // NewCloudInitGenerator returns new CloudInitGenerator.
    81  func NewCloudInitGenerator(config *types.VMConfig, isoDir string) *CloudInitGenerator {
    82  	return &CloudInitGenerator{
    83  		config: config,
    84  		isoDir: isoDir,
    85  	}
    86  }
    87  
    88  func (g *CloudInitGenerator) generateMetaData() ([]byte, error) {
    89  	m := map[string]interface{}{
    90  		"instance-id":    fmt.Sprintf("%s.%s", g.config.PodName, g.config.PodNamespace),
    91  		"local-hostname": g.config.PodName,
    92  	}
    93  
    94  	// TODO: get rid of this if. Use descriptor for cloud-init image types.
    95  	if g.config.ParsedAnnotations.CDImageType == types.CloudInitImageTypeConfigDrive {
    96  		m["uuid"] = m["instance-id"]
    97  		m["hostname"] = m["local-hostname"]
    98  	}
    99  
   100  	if len(g.config.ParsedAnnotations.SSHKeys) != 0 {
   101  		var keys []string
   102  		for _, key := range g.config.ParsedAnnotations.SSHKeys {
   103  			keys = append(keys, key)
   104  		}
   105  		m["public-keys"] = keys
   106  	}
   107  
   108  	for k, v := range g.config.ParsedAnnotations.MetaData {
   109  		m[k] = v
   110  	}
   111  
   112  	r, err := json.Marshal(m)
   113  	if err != nil {
   114  		return nil, fmt.Errorf("error marshaling meta-data: %v", err)
   115  	}
   116  
   117  	return r, nil
   118  }
   119  
   120  func (g *CloudInitGenerator) generateUserData(volumeMap diskPathMap) ([]byte, error) {
   121  	symlinkScript := g.generateSymlinkScript(volumeMap)
   122  	mounts, mountScript := g.generateMounts(volumeMap)
   123  
   124  	if userDataScript := g.config.ParsedAnnotations.UserDataScript; userDataScript != "" {
   125  		fullMountScript := ""
   126  		switch {
   127  		case mountScript != "" && symlinkScript != "":
   128  			fullMountScript = symlinkScript + "\n" + mountScript
   129  		case mountScript != "":
   130  			fullMountScript = mountScript
   131  		case symlinkScript != "":
   132  			fullMountScript = symlinkScript
   133  		}
   134  		return []byte(strings.Replace(userDataScript, mountScriptSubst, fullMountScript, -1)), nil
   135  	}
   136  
   137  	userData := make(map[string]interface{})
   138  	for k, v := range g.config.ParsedAnnotations.UserData {
   139  		userData[k] = v
   140  	}
   141  
   142  	mounts = utils.Merge(userData["mounts"], mounts).([]interface{})
   143  	if len(mounts) != 0 {
   144  		userData["mounts"] = g.fixMounts(volumeMap, mounts)
   145  	}
   146  
   147  	writeFilesUpdater := newWriteFilesUpdater(g.config.Mounts)
   148  	writeFilesUpdater.addSecrets()
   149  	writeFilesUpdater.addConfigMapEntries()
   150  	writeFilesUpdater.addFileLikeMounts()
   151  	if symlinkScript != "" {
   152  		writeFilesUpdater.addSymlinkScript(symlinkScript)
   153  		userData["runcmd"] = utils.Merge(userData["runcmd"], []string{
   154  			shellquote.Join(symlinkFileLocation),
   155  			linkStartupScriptTemplate.MustExecuteToString(map[string]string{
   156  				"StartupScript": symlinkFileLocation,
   157  			}),
   158  		})
   159  	}
   160  	if mountScript != "" {
   161  		writeFilesUpdater.addMountScript(mountScript)
   162  	}
   163  	if envContent := g.generateEnvVarsContent(); envContent != "" {
   164  		writeFilesUpdater.addEnvironmentFile(envContent)
   165  	}
   166  	writeFilesUpdater.updateUserData(userData)
   167  
   168  	r := []byte{}
   169  	if len(userData) != 0 {
   170  		var err error
   171  		r, err = yaml.Marshal(userData)
   172  		if err != nil {
   173  			return nil, fmt.Errorf("error marshalling user-data: %v", err)
   174  		}
   175  	}
   176  	return []byte("#cloud-config\n" + string(r)), nil
   177  }
   178  
   179  func (g *CloudInitGenerator) generateNetworkConfiguration() ([]byte, error) {
   180  	if g.config.ParsedAnnotations.ForceDHCPNetworkConfig || g.config.RootVolumeDevice() != nil {
   181  		// Don't use cloud-init network config if asked not
   182  		// to do so.
   183  		// Also, we don't use network config with persistent
   184  		// rootfs for now because with some cloud-init
   185  		// implementations it's applied only once
   186  		return nil, nil
   187  	}
   188  	// TODO: get rid of this switch. Use descriptor for cloud-init image types.
   189  	switch g.config.ParsedAnnotations.CDImageType {
   190  	case types.CloudInitImageTypeNoCloud:
   191  		return g.generateNetworkConfigurationNoCloud()
   192  	case types.CloudInitImageTypeConfigDrive:
   193  		return g.generateNetworkConfigurationConfigDrive()
   194  	}
   195  
   196  	return nil, fmt.Errorf("unknown cloud-init config image type: %q", g.config.ParsedAnnotations.CDImageType)
   197  }
   198  
   199  func (g *CloudInitGenerator) generateNetworkConfigurationNoCloud() ([]byte, error) {
   200  	if g.config.ContainerSideNetwork == nil {
   201  		// This can only happen during integration tests
   202  		// where a dummy sandbox is used
   203  		return []byte("version: 1\n"), nil
   204  	}
   205  	cniResult := g.config.ContainerSideNetwork.Result
   206  
   207  	var config []map[string]interface{}
   208  
   209  	// physical interfaces
   210  	for i, iface := range cniResult.Interfaces {
   211  		if iface.Sandbox == "" {
   212  			// skip host interfaces
   213  			continue
   214  		}
   215  		subnets := g.getSubnetsForNthInterface(i, cniResult)
   216  		mtu, err := mtuForMacAddress(iface.Mac, g.config.ContainerSideNetwork.Interfaces)
   217  		if err != nil {
   218  			return nil, err
   219  		}
   220  		interfaceConf := map[string]interface{}{
   221  			"type":        "physical",
   222  			"name":        iface.Name,
   223  			"mac_address": iface.Mac,
   224  			"subnets":     subnets,
   225  			"mtu":         mtu,
   226  		}
   227  		config = append(config, interfaceConf)
   228  	}
   229  
   230  	// dns
   231  	dnsData := getDNSData(cniResult.DNS)
   232  	if dnsData != nil {
   233  		config = append(config, dnsData...)
   234  	}
   235  
   236  	r, err := yaml.Marshal(map[string]interface{}{
   237  		"config": config,
   238  	})
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	return []byte("version: 1\n" + string(r)), nil
   243  }
   244  
   245  func (g *CloudInitGenerator) getSubnetsForNthInterface(interfaceNo int, cniResult *cnicurrent.Result) []map[string]interface{} {
   246  	var subnets []map[string]interface{}
   247  	routes := append(cniResult.Routes[:0:0], cniResult.Routes...)
   248  	gotDefault := false
   249  	for _, ipConfig := range cniResult.IPs {
   250  		if ipConfig.Interface == interfaceNo {
   251  			subnet := map[string]interface{}{
   252  				"type":    "static",
   253  				"address": ipConfig.Address.IP.String(),
   254  				"netmask": net.IP(ipConfig.Address.Mask).String(),
   255  			}
   256  
   257  			var subnetRoutes []map[string]interface{}
   258  			// iterate on routes slice in reverse order because at
   259  			// the end of loop found element will be removed from slice
   260  			allRoutesLen := len(routes)
   261  			for i := range routes {
   262  				cniRoute := routes[allRoutesLen-1-i]
   263  				var gw net.IP
   264  				if cniRoute.GW != nil && ipConfig.Address.Contains(cniRoute.GW) {
   265  					gw = cniRoute.GW
   266  				} else if cniRoute.GW == nil && !ipConfig.Gateway.IsUnspecified() {
   267  					gw = ipConfig.Gateway
   268  				} else {
   269  					continue
   270  				}
   271  				if ones, _ := cniRoute.Dst.Mask.Size(); ones == 0 {
   272  					if gotDefault {
   273  						glog.Warning("cloud-init: got more than one default route, using only the first one")
   274  						continue
   275  					}
   276  					gotDefault = true
   277  				}
   278  				route := map[string]interface{}{
   279  					"network": cniRoute.Dst.IP.String(),
   280  					"netmask": net.IP(cniRoute.Dst.Mask).String(),
   281  					"gateway": gw.String(),
   282  				}
   283  				subnetRoutes = append(subnetRoutes, route)
   284  				routes = append(routes[:allRoutesLen-1-i], routes[allRoutesLen-i:]...)
   285  			}
   286  			if subnetRoutes != nil {
   287  				subnet["routes"] = subnetRoutes
   288  			}
   289  
   290  			subnets = append(subnets, subnet)
   291  		}
   292  	}
   293  
   294  	// fallback to dhcp - should never happen, we always should have IPs
   295  	if subnets == nil {
   296  		subnets = append(subnets, map[string]interface{}{
   297  			"type": "dhcp",
   298  		})
   299  	}
   300  
   301  	return subnets
   302  }
   303  
   304  func getDNSData(cniDNS cnitypes.DNS) []map[string]interface{} {
   305  	var dnsData []map[string]interface{}
   306  	if cniDNS.Nameservers != nil {
   307  		dnsData = append(dnsData, map[string]interface{}{
   308  			"type":    "nameserver",
   309  			"address": cniDNS.Nameservers,
   310  		})
   311  		if cniDNS.Search != nil {
   312  			dnsData[0]["search"] = cniDNS.Search
   313  		}
   314  	}
   315  	return dnsData
   316  }
   317  
   318  func (g *CloudInitGenerator) generateNetworkConfigurationConfigDrive() ([]byte, error) {
   319  	if g.config.ContainerSideNetwork == nil {
   320  		// This can only happen during integration tests
   321  		// where a dummy sandbox is used
   322  		return []byte("{}"), nil
   323  	}
   324  	cniResult := g.config.ContainerSideNetwork.Result
   325  
   326  	config := make(map[string]interface{})
   327  
   328  	// links
   329  	var links []map[string]interface{}
   330  	for _, iface := range cniResult.Interfaces {
   331  		if iface.Sandbox == "" {
   332  			// skip host interfaces
   333  			continue
   334  		}
   335  		mtu, err := mtuForMacAddress(iface.Mac, g.config.ContainerSideNetwork.Interfaces)
   336  		if err != nil {
   337  			return nil, err
   338  		}
   339  		linkConf := map[string]interface{}{
   340  			"type":                 "phy",
   341  			"id":                   iface.Name,
   342  			"ethernet_mac_address": iface.Mac,
   343  			"mtu":                  mtu,
   344  		}
   345  		links = append(links, linkConf)
   346  	}
   347  	config["links"] = links
   348  
   349  	var networks []map[string]interface{}
   350  	for i, ipConfig := range cniResult.IPs {
   351  		netConf := map[string]interface{}{
   352  			"id": fmt.Sprintf("net-%d", i),
   353  			// config from openstack have as network_id network uuid
   354  			"network_id": fmt.Sprintf("net-%d", i),
   355  			"type":       fmt.Sprintf("ipv%s", ipConfig.Version),
   356  			"link":       cniResult.Interfaces[ipConfig.Interface].Name,
   357  			"ip_address": ipConfig.Address.IP.String(),
   358  			"netmask":    net.IP(ipConfig.Address.Mask).String(),
   359  		}
   360  
   361  		routes := routesForIP(ipConfig.Address, cniResult.Routes)
   362  		if routes != nil {
   363  			netConf["routes"] = routes
   364  		}
   365  
   366  		networks = append(networks, netConf)
   367  	}
   368  	config["networks"] = networks
   369  
   370  	dnsData := getDNSData(cniResult.DNS)
   371  	if dnsData != nil {
   372  		config["services"] = dnsData
   373  	}
   374  
   375  	r, err := json.Marshal(config)
   376  	if err != nil {
   377  		return nil, fmt.Errorf("error marshaling network configuration: %v", err)
   378  	}
   379  	return r, nil
   380  }
   381  
   382  func routesForIP(sourceIP net.IPNet, allRoutes []*cnitypes.Route) []map[string]interface{} {
   383  	var routes []map[string]interface{}
   384  
   385  	// NOTE: at the moment on cni result level there is no distinction
   386  	// for which interface particular route should be set,
   387  	// so we are returning there all routes with gateway accessible
   388  	// by particular source ip address.
   389  	for _, route := range allRoutes {
   390  		if sourceIP.Contains(route.GW) {
   391  			routes = append(routes, map[string]interface{}{
   392  				"network": route.Dst.IP.String(),
   393  				"netmask": net.IP(route.Dst.Mask).String(),
   394  				"gateway": route.GW.String(),
   395  			})
   396  		}
   397  	}
   398  
   399  	return routes
   400  }
   401  
   402  func mtuForMacAddress(mac string, ifaces []*network.InterfaceDescription) (uint16, error) {
   403  	for _, iface := range ifaces {
   404  		if iface.HardwareAddr.String() == strings.ToLower(mac) {
   405  			return iface.MTU, nil
   406  		}
   407  	}
   408  	return 0, fmt.Errorf("interface with mac address %q not found in ContainerSideNetwork", mac)
   409  }
   410  
   411  // IsoPath returns a full path to iso image with configuration for VM pod.
   412  func (g *CloudInitGenerator) IsoPath() string {
   413  	return filepath.Join(g.isoDir, fmt.Sprintf("config-%s.iso", g.config.DomainUUID))
   414  }
   415  
   416  // DiskDef returns a DomainDisk definition for Cloud Init ISO image to be included
   417  // in VM pod libvirt domain definition.
   418  func (g *CloudInitGenerator) DiskDef() *libvirtxml.DomainDisk {
   419  	return &libvirtxml.DomainDisk{
   420  		Device:   "cdrom",
   421  		Driver:   &libvirtxml.DomainDiskDriver{Name: "qemu", Type: "raw"},
   422  		Source:   &libvirtxml.DomainDiskSource{File: &libvirtxml.DomainDiskSourceFile{File: g.IsoPath()}},
   423  		ReadOnly: &libvirtxml.DomainDiskReadOnly{},
   424  	}
   425  }
   426  
   427  // GenerateImage collects metadata, userdata and network configuration and uses
   428  // them to prepare an ISO image for NoCloud or ConfigDrive selecting the type
   429  // using an info from pod annotations.
   430  func (g *CloudInitGenerator) GenerateImage(volumeMap diskPathMap) error {
   431  	tmpDir, err := ioutil.TempDir("", "config-")
   432  	if err != nil {
   433  		return fmt.Errorf("can't create temp dir for config image: %v", err)
   434  	}
   435  	defer os.RemoveAll(tmpDir)
   436  
   437  	var metaData, userData, networkConfiguration []byte
   438  	metaData, err = g.generateMetaData()
   439  	if err == nil {
   440  		userData, err = g.generateUserData(volumeMap)
   441  	}
   442  	if err == nil {
   443  		networkConfiguration, err = g.generateNetworkConfiguration()
   444  	}
   445  	if err != nil {
   446  		return err
   447  	}
   448  
   449  	var userDataLocation, metaDataLocation, networkConfigLocation string
   450  	var volumeName string
   451  
   452  	// TODO: get rid of this switch. Use descriptor for cloud-init image types.
   453  	switch g.config.ParsedAnnotations.CDImageType {
   454  	case types.CloudInitImageTypeNoCloud:
   455  		userDataLocation = "user-data"
   456  		metaDataLocation = "meta-data"
   457  		networkConfigLocation = "network-config"
   458  		volumeName = "cidata"
   459  	case types.CloudInitImageTypeConfigDrive:
   460  		userDataLocation = "openstack/latest/user_data"
   461  		metaDataLocation = "openstack/latest/meta_data.json"
   462  		networkConfigLocation = "openstack/latest/network_data.json"
   463  		volumeName = "config-2"
   464  	default:
   465  		// that should newer happen, as imageType should be validated
   466  		// already earlier
   467  		return fmt.Errorf("unknown cloud-init config image type: %q", g.config.ParsedAnnotations.CDImageType)
   468  	}
   469  
   470  	fileMap := map[string][]byte{
   471  		userDataLocation: userData,
   472  		metaDataLocation: metaData,
   473  	}
   474  	if networkConfiguration != nil {
   475  		fileMap[networkConfigLocation] = networkConfiguration
   476  	}
   477  	if err := fs.WriteFiles(tmpDir, fileMap); err != nil {
   478  		return fmt.Errorf("can't write user-data: %v", err)
   479  	}
   480  
   481  	if err := os.MkdirAll(g.isoDir, 0777); err != nil {
   482  		return fmt.Errorf("error making iso directory %q: %v", g.isoDir, err)
   483  	}
   484  
   485  	if err := fs.GenIsoImage(g.IsoPath(), volumeName, tmpDir); err != nil {
   486  		if rmErr := os.Remove(g.IsoPath()); rmErr != nil {
   487  			glog.Warningf("Error removing iso file %s: %v", g.IsoPath(), rmErr)
   488  		}
   489  		return fmt.Errorf("error generating iso image: %v", err)
   490  	}
   491  
   492  	return nil
   493  }
   494  
   495  func (g *CloudInitGenerator) generateEnvVarsContent() string {
   496  	var buffer bytes.Buffer
   497  	for _, entry := range g.config.Environment {
   498  		buffer.WriteString(fmt.Sprintf("%s=%s\n", entry.Key, entry.Value))
   499  	}
   500  
   501  	return buffer.String()
   502  }
   503  
   504  func isRegularFile(path string) bool {
   505  	fi, err := os.Stat(path)
   506  	if err != nil {
   507  		return false
   508  	}
   509  	return fi.Mode().IsRegular()
   510  }
   511  
   512  func (g *CloudInitGenerator) generateSymlinkScript(volumeMap diskPathMap) string {
   513  	var symlinkLines []string
   514  	for _, dev := range g.config.VolumeDevices {
   515  		if dev.IsRoot() {
   516  			// special case for the persistent rootfs
   517  			continue
   518  		}
   519  		dpath, found := volumeMap[dev.UUID()]
   520  		if !found {
   521  			glog.Warningf("Couldn't determine the path for device %q inside the VM (target path inside the VM: %q)", dev.HostPath, dev.DevicePath)
   522  			continue
   523  		}
   524  		line := linkBlockDeviceScriptTemplate.MustExecuteToString(map[string]string{
   525  			"SysfsPath":  dpath.sysfsPath,
   526  			"DevicePath": dev.DevicePath,
   527  		})
   528  		symlinkLines = append(symlinkLines, line)
   529  	}
   530  	return makeScript(symlinkLines)
   531  }
   532  
   533  func (g *CloudInitGenerator) fixMounts(volumeMap diskPathMap, mounts []interface{}) []interface{} {
   534  	devMap := make(map[string]string)
   535  	for _, dev := range g.config.VolumeDevices {
   536  		if dev.IsRoot() {
   537  			// special case for the persistent rootfs
   538  			continue
   539  		}
   540  		dpath, found := volumeMap[dev.UUID()]
   541  		if !found {
   542  			glog.Warningf("Couldn't determine the path for device %q inside the VM (target path inside the VM: %q)", dev.HostPath, dev.DevicePath)
   543  			continue
   544  		}
   545  		devMap[dev.DevicePath] = dpath.devPath
   546  	}
   547  	if len(devMap) == 0 {
   548  		return mounts
   549  	}
   550  
   551  	var r []interface{}
   552  	for _, item := range mounts {
   553  		m, ok := item.([]interface{})
   554  		if !ok || len(m) == 0 {
   555  			r = append(r, item)
   556  			continue
   557  		}
   558  		devPath, ok := m[0].(string)
   559  		if !ok {
   560  			r = append(r, item)
   561  			continue
   562  		}
   563  		mapTo, found := devMap[devPath]
   564  		if !found {
   565  			r = append(r, item)
   566  			continue
   567  		}
   568  		r = append(r, append([]interface{}{mapTo}, m[1:]...))
   569  	}
   570  	return r
   571  }
   572  
   573  func (g *CloudInitGenerator) generateMounts(volumeMap diskPathMap) ([]interface{}, string) {
   574  	var r []interface{}
   575  	var mountScriptLines []string
   576  	for _, m := range g.config.Mounts {
   577  		// Skip file based mounts (including secrets and config maps).
   578  		if isRegularFile(m.HostPath) ||
   579  			strings.Contains(m.HostPath, "kubernetes.io~secret") ||
   580  			strings.Contains(m.HostPath, "kubernetes.io~configmap") {
   581  			continue
   582  		}
   583  
   584  		mountInfo, mountScriptLine, err := generateFlexvolumeMounts(volumeMap, m)
   585  		if err != nil {
   586  			if !os.IsNotExist(err) {
   587  				glog.Errorf("Can't mount directory %q to %q inside the VM: %v", m.HostPath, m.ContainerPath, err)
   588  				continue
   589  			}
   590  
   591  			// Fs based volume
   592  			mountInfo, mountScriptLine, err = generateFsBasedVolumeMounts(m)
   593  			if err != nil {
   594  				glog.Errorf("Can't mount directory %q to %q inside the VM: %v", m.HostPath, m.ContainerPath, err)
   595  				continue
   596  			}
   597  		}
   598  
   599  		r = append(r, mountInfo)
   600  		mountScriptLines = append(mountScriptLines, mountScriptLine)
   601  	}
   602  
   603  	return r, makeScript(mountScriptLines)
   604  }
   605  
   606  func generateFlexvolumeMounts(volumeMap diskPathMap, mount types.VMMount) ([]interface{}, string, error) {
   607  	uuid, part, err := flexvolume.GetFlexvolumeInfo(mount.HostPath)
   608  	if err != nil {
   609  		// If the error is NotExist, return the original error
   610  		if os.IsNotExist(err) {
   611  			return nil, "", err
   612  		}
   613  		err = fmt.Errorf("can't get flexvolume uuid: %v", err)
   614  		return nil, "", err
   615  	}
   616  	dpath, found := volumeMap[uuid]
   617  	if !found {
   618  		err = fmt.Errorf("no device found for flexvolume uuid %q", uuid)
   619  		return nil, "", err
   620  	}
   621  	if part < 0 {
   622  		part = 1
   623  	}
   624  	devPath := dpath.devPath
   625  	mountDevSuffix := ""
   626  	if part != 0 {
   627  		devPath += fmt.Sprintf("-part%d", part)
   628  		mountDevSuffix += strconv.Itoa(part)
   629  	}
   630  	mountScriptLine := mountDevScriptTemplate.MustExecuteToString(map[string]string{
   631  		"ContainerPath": mount.ContainerPath,
   632  		"SysfsPath":     dpath.sysfsPath,
   633  		"DevSuffix":     mountDevSuffix,
   634  	})
   635  	return []interface{}{devPath, mount.ContainerPath}, mountScriptLine, nil
   636  }
   637  
   638  func generateFsBasedVolumeMounts(mount types.VMMount) ([]interface{}, string, error) {
   639  	mountTag := path.Base(mount.ContainerPath)
   640  	fsMountScript := mountFSScriptTemplate.MustExecuteToString(map[string]string{
   641  		"ContainerPath": mount.ContainerPath,
   642  		"MountTag":      mountTag,
   643  	})
   644  	r := []interface{}{mountTag, mount.ContainerPath, "9p", "trans=virtio"}
   645  	return r, fsMountScript, nil
   646  }
   647  
   648  type writeFilesUpdater struct {
   649  	entries []interface{}
   650  	mounts  []types.VMMount
   651  }
   652  
   653  func newWriteFilesUpdater(mounts []types.VMMount) *writeFilesUpdater {
   654  	return &writeFilesUpdater{
   655  		mounts: mounts,
   656  	}
   657  }
   658  
   659  func (u *writeFilesUpdater) put(entry interface{}) {
   660  	u.entries = append(u.entries, entry)
   661  }
   662  
   663  func (u *writeFilesUpdater) putPlainText(path string, content string, perms os.FileMode) {
   664  	u.put(map[string]interface{}{
   665  		"path":        path,
   666  		"content":     content,
   667  		"permissions": fmt.Sprintf("%#o", uint32(perms)),
   668  	})
   669  }
   670  
   671  func (u *writeFilesUpdater) putBase64(path string, content []byte, perms os.FileMode) {
   672  	encodedContent := base64.StdEncoding.EncodeToString(content)
   673  	u.put(map[string]interface{}{
   674  		"path":        path,
   675  		"content":     encodedContent,
   676  		"encoding":    "b64",
   677  		"permissions": fmt.Sprintf("%#o", uint32(perms)),
   678  	})
   679  }
   680  
   681  func (u *writeFilesUpdater) updateUserData(userData map[string]interface{}) {
   682  	if len(u.entries) == 0 {
   683  		return
   684  	}
   685  
   686  	writeFiles := utils.Merge(userData["write_files"], u.entries).([]interface{})
   687  	if len(writeFiles) != 0 {
   688  		userData["write_files"] = writeFiles
   689  	}
   690  }
   691  
   692  func (u *writeFilesUpdater) addSecrets() {
   693  	u.addFilesForVolumeType("secret")
   694  }
   695  
   696  func (u *writeFilesUpdater) addConfigMapEntries() {
   697  	u.addFilesForVolumeType("configmap")
   698  }
   699  
   700  func (u *writeFilesUpdater) addFileLikeMounts() {
   701  	for _, mount := range u.filterMounts(func(path string) bool {
   702  		fi, err := os.Stat(path)
   703  		switch {
   704  		case err != nil:
   705  			return false
   706  		case fi.Mode().IsRegular():
   707  			return true
   708  		}
   709  		return false
   710  	}) {
   711  		content, err := ioutil.ReadFile(mount.HostPath)
   712  		if err != nil {
   713  			glog.Warningf("Error during reading content of '%s' file: %v", mount.HostPath, err)
   714  			continue
   715  		}
   716  
   717  		glog.V(3).Infof("Adding file '%s' as volume: %s", mount.HostPath, mount.ContainerPath)
   718  		u.putBase64(mount.ContainerPath, content, 0644)
   719  	}
   720  }
   721  
   722  func (u *writeFilesUpdater) addFilesForVolumeType(suffix string) {
   723  	filter := "volumes/kubernetes.io~" + suffix + "/"
   724  	for _, mount := range u.filterMounts(func(path string) bool {
   725  		return strings.Contains(path, filter)
   726  	}) {
   727  		u.addFilesForMount(mount)
   728  	}
   729  }
   730  
   731  func (u *writeFilesUpdater) addSymlinkScript(content string) {
   732  	u.putPlainText(symlinkFileLocation, content, 0755)
   733  }
   734  
   735  func (u *writeFilesUpdater) addMountScript(content string) {
   736  	u.putPlainText(mountFileLocation, content, 0755)
   737  }
   738  
   739  func (u *writeFilesUpdater) addEnvironmentFile(content string) {
   740  	u.putPlainText(envFileLocation, content, 0644)
   741  }
   742  
   743  func (u *writeFilesUpdater) addFilesForMount(mount types.VMMount) []interface{} {
   744  	var writeFiles []interface{}
   745  
   746  	addFileContent := func(fullPath string) error {
   747  		content, err := ioutil.ReadFile(fullPath)
   748  		if err != nil {
   749  			return err
   750  		}
   751  		stat, err := os.Stat(fullPath)
   752  		if err != nil {
   753  			return err
   754  		}
   755  		relativePath := fullPath[len(mount.HostPath)+1:]
   756  		u.putBase64(path.Join(mount.ContainerPath, relativePath), content, stat.Mode())
   757  		return nil
   758  	}
   759  
   760  	glog.V(3).Infof("Scanning %s for files", mount.HostPath)
   761  	if err := scanDirectory(mount.HostPath, addFileContent); err != nil {
   762  		glog.Errorf("Error while scanning directory %s: %v", mount.HostPath, err)
   763  	}
   764  	glog.V(3).Infof("Found %d entries", len(writeFiles))
   765  
   766  	return writeFiles
   767  }
   768  
   769  func (u *writeFilesUpdater) filterMounts(filter func(string) bool) []types.VMMount {
   770  	var r []types.VMMount
   771  	for _, mount := range u.mounts {
   772  		if filter(mount.HostPath) {
   773  			r = append(r, mount)
   774  		}
   775  	}
   776  	return r
   777  }
   778  
   779  func scanDirectory(dirPath string, callback func(string) error) error {
   780  	entries, err := ioutil.ReadDir(dirPath)
   781  	if err != nil {
   782  		return err
   783  	}
   784  
   785  	for _, entry := range entries {
   786  		fullPath := path.Join(dirPath, entry.Name())
   787  
   788  		switch {
   789  		case entry.Mode().IsDir():
   790  			glog.V(3).Infof("Scanning directory: %s", entry.Name())
   791  			if err := scanDirectory(fullPath, callback); err != nil {
   792  				return err
   793  			}
   794  			glog.V(3).Infof("Leaving directory: %s", entry.Name())
   795  		case entry.Mode().IsRegular():
   796  			glog.V(3).Infof("Found regular file: %s", entry.Name())
   797  			err := callback(fullPath)
   798  			if err != nil {
   799  				return err
   800  			}
   801  			continue
   802  		case entry.Mode()&os.ModeSymlink != 0:
   803  			glog.V(3).Infof("Found symlink: %s", entry.Name())
   804  			fi, err := os.Stat(fullPath)
   805  			switch {
   806  			case err != nil:
   807  				return err
   808  			case fi.Mode().IsRegular():
   809  				err = callback(fullPath)
   810  				if err != nil {
   811  					return err
   812  				}
   813  			case fi.Mode().IsDir():
   814  				glog.V(3).Info("... which points to directory, going deeper ...")
   815  
   816  				// NOTE: this does not need to be protected against loops
   817  				// because it's prepared by kubelet in safe manner (if it's not
   818  				// it's bug on kubelet side
   819  				if err := scanDirectory(fullPath, callback); err != nil {
   820  					return err
   821  				}
   822  				glog.V(3).Infof("... came back from symlink to directory: %s", entry.Name())
   823  			default:
   824  				glog.V(3).Info("... but it's pointing to something other than directory or regular file")
   825  			}
   826  		}
   827  	}
   828  
   829  	return nil
   830  }
   831  
   832  func makeScript(lines []string) string {
   833  	if len(lines) != 0 {
   834  		return fmt.Sprintf("#!/bin/sh\n%s\n", strings.Join(lines, "\n"))
   835  	}
   836  	return ""
   837  }