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 }