github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/libvirttools/cloudinit_test.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/json"
    22  	"io/ioutil"
    23  	"net"
    24  	"os"
    25  	"path/filepath"
    26  	"reflect"
    27  	"strconv"
    28  	"testing"
    29  
    30  	cnitypes "github.com/containernetworking/cni/pkg/types"
    31  	cnicurrent "github.com/containernetworking/cni/pkg/types/current"
    32  	"github.com/davecgh/go-spew/spew"
    33  	"github.com/ghodss/yaml"
    34  
    35  	"github.com/Mirantis/virtlet/pkg/metadata/types"
    36  	"github.com/Mirantis/virtlet/pkg/network"
    37  	"github.com/Mirantis/virtlet/pkg/utils"
    38  	testutils "github.com/Mirantis/virtlet/pkg/utils/testing"
    39  	"github.com/Mirantis/virtlet/tests/gm"
    40  	libvirtxml "github.com/libvirt/libvirt-go-xml"
    41  )
    42  
    43  type fakeFlexvolume struct {
    44  	uuid string
    45  	part int
    46  	path string
    47  }
    48  
    49  func newFakeFlexvolume(t *testing.T, parentDir string, uuid string, part int) *fakeFlexvolume {
    50  	info := map[string]string{"uuid": uuid}
    51  	if part >= 0 {
    52  		info["part"] = strconv.Itoa(part)
    53  	}
    54  	volDir := filepath.Join(parentDir, uuid)
    55  	if err := os.MkdirAll(volDir, 0777); err != nil {
    56  		t.Fatalf("MkdirAll(): %q: %v", volDir, err)
    57  	}
    58  	infoPath := filepath.Join(volDir, "virtlet-flexvolume.json")
    59  	if err := utils.WriteJSON(infoPath, info, 0777); err != nil {
    60  		t.Fatalf("WriteJSON(): %q: %v", infoPath, err)
    61  	}
    62  	return &fakeFlexvolume{
    63  		uuid: uuid,
    64  		part: part,
    65  		path: volDir,
    66  	}
    67  }
    68  
    69  func buildNetworkedPodConfig(cniResult *cnicurrent.Result, imageTypeName string) *types.VMConfig {
    70  	var descs []*network.InterfaceDescription
    71  	for _, iface := range cniResult.Interfaces {
    72  		if iface.Sandbox != "" {
    73  			mac, _ := net.ParseMAC(iface.Mac)
    74  			descs = append(descs, &network.InterfaceDescription{
    75  				HardwareAddr: mac,
    76  				MTU:          1500,
    77  			})
    78  		}
    79  	}
    80  	return &types.VMConfig{
    81  		PodName:           "foo",
    82  		PodNamespace:      "default",
    83  		ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageType(imageTypeName)},
    84  		ContainerSideNetwork: &network.ContainerSideNetwork{
    85  			Result:     cniResult,
    86  			Interfaces: descs,
    87  		},
    88  	}
    89  }
    90  
    91  func TestCloudInitGenerator(t *testing.T) {
    92  	tmpDir, err := ioutil.TempDir("", "fake-flexvol")
    93  	if err != nil {
    94  		t.Fatalf("TempDir(): %v", err)
    95  	}
    96  	defer os.RemoveAll(tmpDir)
    97  	vols := []*fakeFlexvolume{
    98  		newFakeFlexvolume(t, tmpDir, "77f29a0e-46af-4188-a6af-9ff8b8a65224", -1),
    99  		newFakeFlexvolume(t, tmpDir, "82b7a880-dc04-48a3-8f2d-0c6249bb53fe", 0),
   100  		newFakeFlexvolume(t, tmpDir, "94ae25c7-62e1-4854-9f9b-9e285c3a5ed9", 2),
   101  	}
   102  	volDevs := []types.VMVolumeDevice{
   103  		{
   104  			DevicePath: "/dev/disk-a",
   105  			HostPath:   vols[0].path,
   106  		},
   107  		{
   108  			DevicePath: "/dev/disk-b",
   109  			HostPath:   vols[1].path,
   110  		},
   111  		{
   112  			DevicePath: "/dev/disk-c",
   113  			HostPath:   vols[2].path,
   114  		},
   115  	}
   116  
   117  	sharedDir := filepath.Join(tmpDir, "640ad329-e533-4ec0-820f-f11b2255bd56")
   118  	if err := os.MkdirAll(sharedDir, 0777); err != nil {
   119  		t.Fatalf("MkdirAll(): %q: %v", sharedDir, err)
   120  	}
   121  
   122  	for _, tc := range []struct {
   123  		name                string
   124  		config              *types.VMConfig
   125  		volumeMap           diskPathMap
   126  		verifyMetaData      bool
   127  		verifyUserData      bool
   128  		verifyNetworkConfig bool
   129  		verifyUserDataStr   bool
   130  	}{
   131  		{
   132  			name: "plain pod",
   133  			config: &types.VMConfig{
   134  				PodName:           "foo",
   135  				PodNamespace:      "default",
   136  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeNoCloud},
   137  			},
   138  			verifyMetaData:      true,
   139  			verifyNetworkConfig: true,
   140  		},
   141  		{
   142  			name: "metadata for configdrive",
   143  			config: &types.VMConfig{
   144  				PodName:           "foo",
   145  				PodNamespace:      "default",
   146  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeConfigDrive},
   147  			},
   148  			verifyMetaData: true,
   149  		},
   150  		{
   151  			name: "pod with ssh keys",
   152  			config: &types.VMConfig{
   153  				PodName:      "foo",
   154  				PodNamespace: "default",
   155  				ParsedAnnotations: &types.VirtletAnnotations{
   156  					SSHKeys:     []string{"key1", "key2"},
   157  					CDImageType: types.CloudInitImageTypeNoCloud,
   158  				},
   159  			},
   160  			verifyMetaData: true,
   161  		},
   162  		{
   163  			name: "pod with ssh keys and meta-data override",
   164  			config: &types.VMConfig{
   165  				PodName:      "foo",
   166  				PodNamespace: "default",
   167  				ParsedAnnotations: &types.VirtletAnnotations{
   168  					SSHKeys: []string{"key1", "key2"},
   169  					MetaData: map[string]interface{}{
   170  						"instance-id": "foobar",
   171  					},
   172  					CDImageType: types.CloudInitImageTypeNoCloud,
   173  				},
   174  			},
   175  			verifyMetaData: true,
   176  		},
   177  		{
   178  			name: "pod with user data",
   179  			config: &types.VMConfig{
   180  				PodName:      "foo",
   181  				PodNamespace: "default",
   182  				ParsedAnnotations: &types.VirtletAnnotations{
   183  					UserData: map[string]interface{}{
   184  						"users": []interface{}{
   185  							map[string]interface{}{
   186  								"name": "cloudy",
   187  							},
   188  						},
   189  					},
   190  					SSHKeys:     []string{"key1", "key2"},
   191  					CDImageType: types.CloudInitImageTypeNoCloud,
   192  				},
   193  			},
   194  			verifyMetaData: true,
   195  			verifyUserData: true,
   196  		},
   197  		{
   198  			name: "pod with env variables",
   199  			config: &types.VMConfig{
   200  				PodName:           "foo",
   201  				PodNamespace:      "default",
   202  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeNoCloud},
   203  				Environment: []types.VMKeyValue{
   204  					{"foo", "bar"},
   205  					{"baz", "abc"},
   206  				},
   207  			},
   208  			verifyMetaData: true,
   209  			verifyUserData: true,
   210  		},
   211  		{
   212  			name: "pod with env variables and user data",
   213  			config: &types.VMConfig{
   214  				PodName:      "foo",
   215  				PodNamespace: "default",
   216  				ParsedAnnotations: &types.VirtletAnnotations{
   217  					UserData: map[string]interface{}{
   218  						"users": []interface{}{
   219  							map[string]interface{}{
   220  								"name": "cloudy",
   221  							},
   222  						},
   223  						"write_files": []interface{}{
   224  							map[string]interface{}{
   225  								"path":    "/etc/foobar",
   226  								"content": "whatever",
   227  							},
   228  						},
   229  					},
   230  					CDImageType: types.CloudInitImageTypeNoCloud,
   231  				},
   232  				Environment: []types.VMKeyValue{
   233  					{"foo", "bar"},
   234  					{"baz", "abc"},
   235  				},
   236  			},
   237  			verifyMetaData: true,
   238  			verifyUserData: true,
   239  		},
   240  		{
   241  			name: "pod with user data script",
   242  			config: &types.VMConfig{
   243  				PodName:      "foo",
   244  				PodNamespace: "default",
   245  				ParsedAnnotations: &types.VirtletAnnotations{
   246  					UserDataScript: "#!/bin/sh\necho hi\n",
   247  					SSHKeys:        []string{"key1", "key2"},
   248  					CDImageType:    types.CloudInitImageTypeNoCloud,
   249  				},
   250  			},
   251  			verifyMetaData:    true,
   252  			verifyUserDataStr: true,
   253  		},
   254  		{
   255  			name: "pod with volumes to mount",
   256  			config: &types.VMConfig{
   257  				PodName:           "foo",
   258  				PodNamespace:      "default",
   259  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeNoCloud},
   260  				Mounts: []types.VMMount{
   261  					{
   262  						ContainerPath: "/opt",
   263  						HostPath:      vols[0].path,
   264  					},
   265  					{
   266  						ContainerPath: "/var/lib/whatever",
   267  						HostPath:      vols[1].path,
   268  					},
   269  					{
   270  						ContainerPath: "/var/lib/foobar",
   271  						HostPath:      vols[2].path,
   272  					},
   273  				},
   274  			},
   275  			volumeMap: diskPathMap{
   276  				vols[0].uuid: {
   277  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:1",
   278  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:1/block/",
   279  				},
   280  				vols[1].uuid: {
   281  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:2",
   282  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:2/block/",
   283  				},
   284  				vols[2].uuid: {
   285  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:3",
   286  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:3/block/",
   287  				},
   288  			},
   289  			verifyMetaData: true,
   290  			verifyUserData: true,
   291  		},
   292  		{
   293  			name: "9pfs volume",
   294  			config: &types.VMConfig{
   295  				PodName:           "foo",
   296  				PodNamespace:      "default",
   297  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeNoCloud},
   298  				Mounts: []types.VMMount{
   299  					{
   300  						ContainerPath: "/opt",
   301  						HostPath:      sharedDir,
   302  					},
   303  				},
   304  			},
   305  			verifyMetaData: true,
   306  			verifyUserData: true,
   307  		},
   308  		{
   309  			name: "pod with volume devices",
   310  			config: &types.VMConfig{
   311  				PodName:           "foo",
   312  				PodNamespace:      "default",
   313  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeNoCloud},
   314  				VolumeDevices:     volDevs,
   315  			},
   316  			volumeMap: diskPathMap{
   317  				volDevs[0].UUID(): {
   318  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:1",
   319  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:1/block/",
   320  				},
   321  				volDevs[1].UUID(): {
   322  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:2",
   323  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:2/block/",
   324  				},
   325  				volDevs[2].UUID(): {
   326  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:3",
   327  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:3/block/",
   328  				},
   329  			},
   330  			verifyMetaData: true,
   331  			verifyUserData: true,
   332  		},
   333  		{
   334  			name: "pod with volume devices and volumes to mount",
   335  			config: &types.VMConfig{
   336  				PodName:      "foo",
   337  				PodNamespace: "default",
   338  				ParsedAnnotations: &types.VirtletAnnotations{
   339  					CDImageType: types.CloudInitImageTypeNoCloud,
   340  					UserData: map[string]interface{}{
   341  						"mounts": []interface{}{
   342  							[]interface{}{"/dev/foo1", "/foo1"},
   343  							[]interface{}{"/dev/disk-a", "/foobar"},
   344  							[]interface{}{"/dev/disk-b", "/foobar"},
   345  						},
   346  					},
   347  				},
   348  				VolumeDevices: volDevs[:2],
   349  				Mounts: []types.VMMount{
   350  					{
   351  						ContainerPath: "/var/lib/foobar",
   352  						HostPath:      vols[2].path,
   353  					},
   354  				},
   355  			},
   356  			volumeMap: diskPathMap{
   357  				volDevs[0].UUID(): {
   358  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:1",
   359  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:1/block/",
   360  				},
   361  				volDevs[1].UUID(): {
   362  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:2",
   363  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:2/block/",
   364  				},
   365  				vols[2].uuid: {
   366  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:3",
   367  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:3/block/",
   368  				},
   369  			},
   370  			verifyMetaData: true,
   371  			verifyUserData: true,
   372  		},
   373  		{
   374  			name: "pod with persistent rootfs",
   375  			config: &types.VMConfig{
   376  				PodName:           "foo",
   377  				PodNamespace:      "default",
   378  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeNoCloud},
   379  				VolumeDevices: []types.VMVolumeDevice{
   380  					{
   381  						DevicePath: "/",
   382  						HostPath:   volDevs[0].HostPath,
   383  					},
   384  				},
   385  			},
   386  			verifyMetaData: true,
   387  			verifyUserData: true,
   388  			// make sure network config is null for the persistent rootfs case
   389  			verifyNetworkConfig: true,
   390  		},
   391  		{
   392  			name: "pod with forced dhcp network config",
   393  			config: &types.VMConfig{
   394  				PodName:           "foo",
   395  				PodNamespace:      "default",
   396  				ParsedAnnotations: &types.VirtletAnnotations{ForceDHCPNetworkConfig: true},
   397  			},
   398  			verifyMetaData: true,
   399  			verifyUserData: true,
   400  			// make sure network config is null
   401  			verifyNetworkConfig: true,
   402  		},
   403  		{
   404  			name: "injecting mount script into user data script",
   405  			config: &types.VMConfig{
   406  				PodName:      "foo",
   407  				PodNamespace: "default",
   408  				ParsedAnnotations: &types.VirtletAnnotations{
   409  					UserDataScript: "#!/bin/sh\necho hi\n@virtlet-mount-script@",
   410  					CDImageType:    types.CloudInitImageTypeNoCloud,
   411  				},
   412  				Mounts: []types.VMMount{
   413  					{
   414  						ContainerPath: "/opt",
   415  						HostPath:      vols[0].path,
   416  					},
   417  				},
   418  			},
   419  			volumeMap: diskPathMap{
   420  				vols[0].uuid: {
   421  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:1",
   422  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:1/block/",
   423  				},
   424  			},
   425  			verifyMetaData:    true,
   426  			verifyUserDataStr: true,
   427  		},
   428  		{
   429  			name: "injecting mount and symlink scripts into user data script",
   430  			config: &types.VMConfig{
   431  				PodName:      "foo",
   432  				PodNamespace: "default",
   433  				ParsedAnnotations: &types.VirtletAnnotations{
   434  					UserDataScript: "#!/bin/sh\necho hi\n@virtlet-mount-script@",
   435  					CDImageType:    types.CloudInitImageTypeNoCloud,
   436  				},
   437  				VolumeDevices: volDevs[1:2],
   438  				Mounts: []types.VMMount{
   439  					{
   440  						ContainerPath: "/opt",
   441  						HostPath:      vols[0].path,
   442  					},
   443  				},
   444  			},
   445  			volumeMap: diskPathMap{
   446  				vols[0].uuid: {
   447  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:1",
   448  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:1/block/",
   449  				},
   450  				volDevs[1].UUID(): {
   451  					devPath:   "/dev/disk/by-path/virtio-pci-0000:00:01.0-scsi-0:0:0:2",
   452  					sysfsPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio*/host*/target*:0:0/*:0:0:2/block/",
   453  				},
   454  			},
   455  			verifyMetaData:    true,
   456  			verifyUserDataStr: true,
   457  		},
   458  		{
   459  			name: "pod with network config",
   460  			config: buildNetworkedPodConfig(&cnicurrent.Result{
   461  				Interfaces: []*cnicurrent.Interface{
   462  					{
   463  						Name:    "cni0",
   464  						Mac:     "00:11:22:33:44:55",
   465  						Sandbox: "/var/run/netns/bae464f1-6ee7-4ee2-826e-33293a9de95e",
   466  					},
   467  					{
   468  						Name:    "ignoreme0",
   469  						Mac:     "00:12:34:56:78:9a",
   470  						Sandbox: "", // host interface
   471  					},
   472  				},
   473  				IPs: []*cnicurrent.IPConfig{
   474  					{
   475  						Version: "4",
   476  						Address: net.IPNet{
   477  							IP:   net.IPv4(1, 1, 1, 1),
   478  							Mask: net.CIDRMask(8, 32),
   479  						},
   480  						Gateway:   net.IPv4(1, 2, 3, 4),
   481  						Interface: 0,
   482  					},
   483  				},
   484  				Routes: []*cnitypes.Route{
   485  					{
   486  						Dst: net.IPNet{
   487  							IP:   net.IPv4zero,
   488  							Mask: net.CIDRMask(0, 32),
   489  						},
   490  						GW: nil,
   491  					},
   492  				},
   493  				DNS: cnitypes.DNS{
   494  					Nameservers: []string{"1.2.3.4"},
   495  					Search:      []string{"some", "search"},
   496  				},
   497  			}, "nocloud"),
   498  			verifyNetworkConfig: true,
   499  		},
   500  		{
   501  			name: "pod with multiple network interfaces",
   502  			config: buildNetworkedPodConfig(&cnicurrent.Result{
   503  				Interfaces: []*cnicurrent.Interface{
   504  					{
   505  						Name:    "cni0",
   506  						Mac:     "00:11:22:33:44:55",
   507  						Sandbox: "/var/run/netns/bae464f1-6ee7-4ee2-826e-33293a9de95e",
   508  					},
   509  					{
   510  						Name:    "cni1",
   511  						Mac:     "00:11:22:33:ab:cd",
   512  						Sandbox: "/var/run/netns/d920d2e2-5849-4c70-b9a6-5e3cb4f831cb",
   513  					},
   514  					{
   515  						Name:    "ignoreme0",
   516  						Mac:     "00:12:34:56:78:9a",
   517  						Sandbox: "", // host interface
   518  					},
   519  				},
   520  				IPs: []*cnicurrent.IPConfig{
   521  					// Note that Gateway addresses are not used because
   522  					// there's no routes with nil gateway
   523  					{
   524  						Version: "4",
   525  						Address: net.IPNet{
   526  							IP:   net.IPv4(1, 1, 1, 1),
   527  							Mask: net.CIDRMask(8, 32),
   528  						},
   529  						Gateway:   net.IPv4(1, 2, 3, 4),
   530  						Interface: 0,
   531  					},
   532  					{
   533  						Version: "4",
   534  						Address: net.IPNet{
   535  							IP:   net.IPv4(192, 168, 100, 42),
   536  							Mask: net.CIDRMask(24, 32),
   537  						},
   538  						Gateway:   net.IPv4(192, 168, 100, 1),
   539  						Interface: 1,
   540  					},
   541  				},
   542  				Routes: []*cnitypes.Route{
   543  					{
   544  						Dst: net.IPNet{
   545  							IP:   net.IPv4zero,
   546  							Mask: net.CIDRMask(0, 32),
   547  						},
   548  						GW: net.IPv4(1, 2, 3, 4),
   549  					},
   550  					// additional route like in flannel case
   551  					{
   552  						Dst: net.IPNet{
   553  							IP:   net.IPv4(1, 2, 0, 0),
   554  							Mask: net.CIDRMask(16, 32),
   555  						},
   556  						GW: net.IPv4(1, 2, 3, 4),
   557  					},
   558  				},
   559  				DNS: cnitypes.DNS{
   560  					Nameservers: []string{"1.2.3.4"},
   561  					Search:      []string{"some", "search"},
   562  				},
   563  			}, "nocloud"),
   564  			verifyNetworkConfig: true,
   565  		},
   566  		{
   567  			name: "pod with network config - configdrive",
   568  			config: buildNetworkedPodConfig(&cnicurrent.Result{
   569  				Interfaces: []*cnicurrent.Interface{
   570  					{
   571  						Name:    "cni0",
   572  						Mac:     "00:11:22:33:44:55",
   573  						Sandbox: "/var/run/netns/bae464f1-6ee7-4ee2-826e-33293a9de95e",
   574  					},
   575  					{
   576  						Name:    "ignoreme0",
   577  						Mac:     "00:12:34:56:78:9a",
   578  						Sandbox: "", // host interface
   579  					},
   580  				},
   581  				IPs: []*cnicurrent.IPConfig{
   582  					{
   583  						Version: "4",
   584  						Address: net.IPNet{
   585  							IP:   net.IPv4(1, 1, 1, 1),
   586  							Mask: net.CIDRMask(8, 32),
   587  						},
   588  						Gateway:   net.IPv4(1, 2, 3, 4),
   589  						Interface: 0,
   590  					},
   591  				},
   592  				Routes: []*cnitypes.Route{
   593  					{
   594  						Dst: net.IPNet{
   595  							IP:   net.IPv4zero,
   596  							Mask: net.CIDRMask(0, 32),
   597  						},
   598  						GW: nil,
   599  					},
   600  				},
   601  				DNS: cnitypes.DNS{
   602  					Nameservers: []string{"1.2.3.4"},
   603  					Search:      []string{"some", "search"},
   604  				},
   605  			}, "configdrive"),
   606  			verifyNetworkConfig: true,
   607  		},
   608  		{
   609  			name: "pod with multiple network interfaces - configdrive",
   610  			config: buildNetworkedPodConfig(&cnicurrent.Result{
   611  				Interfaces: []*cnicurrent.Interface{
   612  					{
   613  						Name:    "cni0",
   614  						Mac:     "00:11:22:33:44:55",
   615  						Sandbox: "/var/run/netns/bae464f1-6ee7-4ee2-826e-33293a9de95e",
   616  					},
   617  					{
   618  						Name:    "cni1",
   619  						Mac:     "00:11:22:33:ab:cd",
   620  						Sandbox: "/var/run/netns/d920d2e2-5849-4c70-b9a6-5e3cb4f831cb",
   621  					},
   622  					{
   623  						Name:    "ignoreme0",
   624  						Mac:     "00:12:34:56:78:9a",
   625  						Sandbox: "", // host interface
   626  					},
   627  				},
   628  				IPs: []*cnicurrent.IPConfig{
   629  					// Note that Gateway addresses are not used because
   630  					// there's no routes with nil gateway
   631  					{
   632  						Version: "4",
   633  						Address: net.IPNet{
   634  							IP:   net.IPv4(1, 1, 1, 1),
   635  							Mask: net.CIDRMask(8, 32),
   636  						},
   637  						Gateway:   net.IPv4(1, 2, 3, 4),
   638  						Interface: 0,
   639  					},
   640  					{
   641  						Version: "4",
   642  						Address: net.IPNet{
   643  							IP:   net.IPv4(192, 168, 100, 42),
   644  							Mask: net.CIDRMask(24, 32),
   645  						},
   646  						Gateway:   net.IPv4(192, 168, 100, 1),
   647  						Interface: 1,
   648  					},
   649  				},
   650  				Routes: []*cnitypes.Route{
   651  					{
   652  						Dst: net.IPNet{
   653  							IP:   net.IPv4zero,
   654  							Mask: net.CIDRMask(0, 32),
   655  						},
   656  						GW: net.IPv4(1, 2, 3, 4),
   657  					},
   658  				},
   659  				DNS: cnitypes.DNS{
   660  					Nameservers: []string{"1.2.3.4"},
   661  					Search:      []string{"some", "search"},
   662  				},
   663  			}, "configdrive"),
   664  			verifyNetworkConfig: true,
   665  		},
   666  	} {
   667  		t.Run(tc.name, func(t *testing.T) {
   668  			// we're not invoking actual iso generation here so "/foobar"
   669  			// as isoDir will do
   670  			g := NewCloudInitGenerator(tc.config, "/foobar")
   671  
   672  			r := map[string]interface{}{}
   673  			if tc.verifyMetaData {
   674  				metaDataBytes, err := g.generateMetaData()
   675  				if err != nil {
   676  					t.Fatalf("generateMetaData(): %v", err)
   677  				}
   678  				var metaData map[string]interface{}
   679  				if err := json.Unmarshal(metaDataBytes, &metaData); err != nil {
   680  					t.Fatalf("Can't unmarshal meta-data: %v", err)
   681  				}
   682  
   683  				r["meta-data"] = metaData
   684  			}
   685  
   686  			userDataBytes, err := g.generateUserData(tc.volumeMap)
   687  			if err != nil {
   688  				t.Fatalf("generateUserData(): %v", err)
   689  			}
   690  
   691  			if tc.verifyUserDataStr {
   692  				r["user-data-str"] = string(userDataBytes)
   693  			}
   694  
   695  			if tc.verifyUserData {
   696  				if !bytes.HasPrefix(userDataBytes, []byte("#cloud-config\n")) {
   697  					t.Errorf("No #cloud-config header")
   698  				}
   699  				var userData map[string]interface{}
   700  				if err := yaml.Unmarshal(userDataBytes, &userData); err != nil {
   701  					t.Fatalf("Can't unmarshal user-data: %v", err)
   702  				}
   703  
   704  				r["user-data"] = userData
   705  			}
   706  
   707  			if tc.verifyNetworkConfig {
   708  				networkConfigBytes, err := g.generateNetworkConfiguration()
   709  				if err != nil {
   710  					t.Fatalf("generateNetworkConfiguration(): %v", err)
   711  				}
   712  				var networkConfig map[string]interface{}
   713  				if err := yaml.Unmarshal(networkConfigBytes, &networkConfig); err != nil {
   714  					t.Fatalf("Can't unmarshal user-data: %v", err)
   715  				}
   716  				r["network-config"] = networkConfig
   717  			}
   718  			gm.Verify(t, gm.NewYamlVerifier(r))
   719  		})
   720  	}
   721  }
   722  
   723  func TestCloudInitDiskDef(t *testing.T) {
   724  	g := NewCloudInitGenerator(&types.VMConfig{
   725  		PodName:           "foo",
   726  		PodNamespace:      "default",
   727  		ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeNoCloud},
   728  	}, "")
   729  	diskDef := g.DiskDef()
   730  	if !reflect.DeepEqual(diskDef, &libvirtxml.DomainDisk{
   731  		Device:   "cdrom",
   732  		Driver:   &libvirtxml.DomainDiskDriver{Name: "qemu", Type: "raw"},
   733  		Source:   &libvirtxml.DomainDiskSource{File: &libvirtxml.DomainDiskSourceFile{File: g.IsoPath()}},
   734  		ReadOnly: &libvirtxml.DomainDiskReadOnly{},
   735  	}) {
   736  		t.Errorf("Bad disk definition:\n%s", spew.Sdump(diskDef))
   737  	}
   738  }
   739  
   740  func TestCloudInitGenerateImage(t *testing.T) {
   741  	for _, tc := range []struct {
   742  		name          string
   743  		vmConfig      *types.VMConfig
   744  		expectedFiles map[string]interface{}
   745  	}{
   746  		{
   747  			name: "nocloud",
   748  			vmConfig: &types.VMConfig{
   749  				PodName:           "foo",
   750  				PodNamespace:      "default",
   751  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeNoCloud},
   752  			},
   753  			expectedFiles: map[string]interface{}{
   754  				"meta-data":      "{\"instance-id\":\"foo.default\",\"local-hostname\":\"foo\"}",
   755  				"network-config": "version: 1\n",
   756  				"user-data":      "#cloud-config\n",
   757  			},
   758  		},
   759  		{
   760  			name: "nocloud with persistent rootfs",
   761  			vmConfig: &types.VMConfig{
   762  				PodName:      "foo",
   763  				PodNamespace: "default",
   764  				VolumeDevices: []types.VMVolumeDevice{
   765  					{
   766  						DevicePath: "/",
   767  						HostPath:   "/dev/loop0",
   768  					},
   769  				},
   770  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeNoCloud},
   771  			},
   772  			expectedFiles: map[string]interface{}{
   773  				"meta-data": "{\"instance-id\":\"foo.default\",\"local-hostname\":\"foo\"}",
   774  				"user-data": "#cloud-config\n",
   775  			},
   776  		},
   777  		{
   778  			name: "configdrive",
   779  			vmConfig: &types.VMConfig{
   780  				PodName:           "foo",
   781  				PodNamespace:      "default",
   782  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeConfigDrive},
   783  			},
   784  			expectedFiles: map[string]interface{}{
   785  				"openstack": map[string]interface{}{
   786  					"latest": map[string]interface{}{
   787  						"meta_data.json":    "{\"hostname\":\"foo\",\"instance-id\":\"foo.default\",\"local-hostname\":\"foo\",\"uuid\":\"foo.default\"}",
   788  						"network_data.json": "{}",
   789  						"user_data":         "#cloud-config\n",
   790  					},
   791  				},
   792  			},
   793  		},
   794  		{
   795  			name: "configdrive with persistent rootfs",
   796  			vmConfig: &types.VMConfig{
   797  				PodName:      "foo",
   798  				PodNamespace: "default",
   799  				VolumeDevices: []types.VMVolumeDevice{
   800  					{
   801  						DevicePath: "/",
   802  						HostPath:   "/dev/loop0",
   803  					},
   804  				},
   805  				ParsedAnnotations: &types.VirtletAnnotations{CDImageType: types.CloudInitImageTypeConfigDrive},
   806  			},
   807  			expectedFiles: map[string]interface{}{
   808  				"openstack": map[string]interface{}{
   809  					"latest": map[string]interface{}{
   810  						"meta_data.json": "{\"hostname\":\"foo\",\"instance-id\":\"foo.default\",\"local-hostname\":\"foo\",\"uuid\":\"foo.default\"}",
   811  						"user_data":      "#cloud-config\n",
   812  					},
   813  				},
   814  			},
   815  		},
   816  	} {
   817  		t.Run(tc.name, func(t *testing.T) {
   818  			tmpDir, err := ioutil.TempDir("", "config-")
   819  			if err != nil {
   820  				t.Fatalf("Can't create temp dir: %v", err)
   821  			}
   822  			defer os.RemoveAll(tmpDir)
   823  
   824  			g := NewCloudInitGenerator(tc.vmConfig, tmpDir)
   825  			if err := g.GenerateImage(nil); err != nil {
   826  				t.Fatalf("GenerateImage(): %v", err)
   827  			}
   828  
   829  			m, err := testutils.IsoToMap(g.IsoPath())
   830  			if err != nil {
   831  				t.Fatalf("IsoToMap(): %v", err)
   832  			}
   833  
   834  			if !reflect.DeepEqual(m, tc.expectedFiles) {
   835  				t.Errorf("Bad iso content:\n%s", spew.Sdump(m))
   836  			}
   837  		})
   838  	}
   839  }
   840  
   841  func TestEnvDataGeneration(t *testing.T) {
   842  	expected := "key=value\n"
   843  	g := NewCloudInitGenerator(&types.VMConfig{
   844  		Environment: []types.VMKeyValue{
   845  			{Key: "key", Value: "value"},
   846  		},
   847  	}, "")
   848  
   849  	output := g.generateEnvVarsContent()
   850  	if output != expected {
   851  		t.Errorf("Bad environment data generated:\n%s\nExpected:\n%s", output, expected)
   852  	}
   853  }
   854  
   855  func verifyWriteFiles(t *testing.T, u *writeFilesUpdater, expectedWriteFiles ...interface{}) {
   856  	userData := make(map[string]interface{})
   857  	u.updateUserData(userData)
   858  	expectedUserData := map[string]interface{}{"write_files": expectedWriteFiles}
   859  	if !reflect.DeepEqual(userData, expectedUserData) {
   860  		t.Errorf("Bad user-data:\n%s\nExpected:\n%s", spew.Sdump(userData), spew.Sdump(expectedUserData))
   861  	}
   862  }
   863  
   864  func withFakeVolumeDir(t *testing.T, subdir string, perms os.FileMode, toRun func(location string)) {
   865  	tmpDir, err := ioutil.TempDir("", "")
   866  	if err != nil {
   867  		t.Fatalf("Can't create temp dir: %v", err)
   868  	}
   869  	defer os.RemoveAll(tmpDir)
   870  
   871  	var location, filePath string
   872  	if subdir != "" {
   873  		location = filepath.Join(tmpDir, subdir)
   874  		if err := os.MkdirAll(location, 0755); err != nil {
   875  			t.Fatalf("Can't create secrets directory in temp dir: %v", err)
   876  		}
   877  		filePath = filepath.Join(location, "file")
   878  	} else {
   879  		filePath = filepath.Join(tmpDir, "file")
   880  		location = filePath
   881  	}
   882  
   883  	f, err := os.Create(filePath)
   884  	if err != nil {
   885  		t.Fatalf("Can't create sample file in temp directory: %v", err)
   886  	}
   887  	if _, err := f.WriteString("test content"); err != nil {
   888  		f.Close()
   889  		t.Fatalf("Error writing test file: %v", err)
   890  	}
   891  	if perms != 0 {
   892  		if err := f.Chmod(perms); err != nil {
   893  			t.Fatalf("Chmod(): %v", err)
   894  		}
   895  	}
   896  	if err := f.Close(); err != nil {
   897  		t.Fatalf("Error closing test file: %v", err)
   898  	}
   899  
   900  	toRun(location)
   901  }
   902  
   903  func TestAddingSecrets(t *testing.T) {
   904  	withFakeVolumeDir(t, "volumes/kubernetes.io~secret/test-volume", 0640, func(location string) {
   905  		u := newWriteFilesUpdater([]types.VMMount{
   906  			{ContainerPath: "/container", HostPath: location},
   907  		})
   908  		u.addSecrets()
   909  		verifyWriteFiles(t, u, map[string]interface{}{
   910  			"path":        "/container/file",
   911  			"content":     "dGVzdCBjb250ZW50",
   912  			"encoding":    "b64",
   913  			"permissions": "0640",
   914  		})
   915  	})
   916  }
   917  
   918  func TestAddingConfigMap(t *testing.T) {
   919  	withFakeVolumeDir(t, "volumes/kubernetes.io~configmap/test-volume", 0, func(location string) {
   920  		u := newWriteFilesUpdater([]types.VMMount{
   921  			{ContainerPath: "/container", HostPath: location},
   922  		})
   923  		u.addConfigMapEntries()
   924  		verifyWriteFiles(t, u, map[string]interface{}{
   925  			"path":        "/container/file",
   926  			"content":     "dGVzdCBjb250ZW50",
   927  			"encoding":    "b64",
   928  			"permissions": "0644",
   929  		})
   930  	})
   931  }
   932  
   933  func TestAddingFileLikeMount(t *testing.T) {
   934  	withFakeVolumeDir(t, "", 0, func(location string) {
   935  		u := newWriteFilesUpdater([]types.VMMount{
   936  			{ContainerPath: "/container", HostPath: location},
   937  		})
   938  		u.addFileLikeMounts()
   939  		verifyWriteFiles(t, u, map[string]interface{}{
   940  			"path":        "/container",
   941  			"content":     "dGVzdCBjb250ZW50",
   942  			"encoding":    "b64",
   943  			"permissions": "0644",
   944  		})
   945  	})
   946  }
   947  
   948  func TestMtuForMacAddress(t *testing.T) {
   949  	interfaces := []*network.InterfaceDescription{
   950  		{
   951  			MTU:          1234,
   952  			HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0xa, 0xb},
   953  		},
   954  	}
   955  
   956  	for _, tc := range []struct {
   957  		mac             string
   958  		shouldHaveError bool
   959  		value           uint16
   960  	}{
   961  		{
   962  			mac:             "00:00:00:00:0a:0b",
   963  			shouldHaveError: false,
   964  			value:           1234,
   965  		},
   966  		{
   967  			mac:             "00:00:00:00:0A:0B",
   968  			shouldHaveError: false,
   969  			value:           1234,
   970  		},
   971  		{
   972  			mac:             "00:00:00:0a:0b:0c",
   973  			shouldHaveError: true,
   974  			value:           0,
   975  		},
   976  	} {
   977  		value, err := mtuForMacAddress(tc.mac, interfaces)
   978  		if err == nil && tc.shouldHaveError {
   979  			t.Errorf("Missing expected error")
   980  		}
   981  		if err != nil && !tc.shouldHaveError {
   982  			t.Errorf("Received unexpected error: %v", err)
   983  		}
   984  		if value != tc.value {
   985  			t.Errorf("Received value %q is diffrent from expected %q", value, tc.value)
   986  		}
   987  	}
   988  }