github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cloudconfig/cloudinit/network_ubuntu_test.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Copyright 2015 Cloudbase Solutions SRL
     3  // Licensed under the AGPLv3, see LICENCE file for details.
     4  
     5  package cloudinit_test
     6  
     7  import (
     8  	"crypto/rand"
     9  	"encoding/hex"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strings"
    16  
    17  	jc "github.com/juju/testing/checkers"
    18  	"github.com/juju/utils/exec"
    19  	gc "gopkg.in/check.v1"
    20  	"gopkg.in/yaml.v2"
    21  
    22  	"github.com/juju/juju/cloudconfig/cloudinit"
    23  	"github.com/juju/juju/container"
    24  	"github.com/juju/juju/network"
    25  	"github.com/juju/juju/testing"
    26  )
    27  
    28  type NetworkUbuntuSuite struct {
    29  	testing.BaseSuite
    30  
    31  	networkInterfacesPythonFile string
    32  	systemNetworkInterfacesFile string
    33  	jujuNetplanFile             string
    34  
    35  	fakeInterfaces []network.InterfaceInfo
    36  
    37  	expectedSampleConfigHeader      string
    38  	expectedSampleConfigTemplate    string
    39  	expectedSampleConfigWriting     string
    40  	expectedSampleUserData          string
    41  	expectedFallbackConfig          string
    42  	expectedBaseConfig              string
    43  	expectedFullNetplanYaml         string
    44  	expectedBaseNetplanYaml         string
    45  	expectedFullNetplan             string
    46  	expectedBaseNetplan             string
    47  	expectedFallbackUserData        string
    48  	tempFolder                      string
    49  	pythonVersions                  []string
    50  	originalSystemNetworkInterfaces string
    51  }
    52  
    53  var _ = gc.Suite(&NetworkUbuntuSuite{})
    54  
    55  func (s *NetworkUbuntuSuite) SetUpTest(c *gc.C) {
    56  	s.BaseSuite.SetUpTest(c)
    57  
    58  	if runtime.GOOS == "windows" {
    59  		c.Skip("This test is for Linux only")
    60  	}
    61  
    62  	s.tempFolder = c.MkDir()
    63  	networkFolder := c.MkDir()
    64  	netplanFolder := c.MkDir()
    65  	s.systemNetworkInterfacesFile = filepath.Join(networkFolder, "system-interfaces")
    66  	s.networkInterfacesPythonFile = filepath.Join(networkFolder, "system-interfaces.py")
    67  	s.jujuNetplanFile = filepath.Join(netplanFolder, "79-juju.yaml")
    68  
    69  	s.fakeInterfaces = []network.InterfaceInfo{{
    70  		InterfaceName:    "any0",
    71  		CIDR:             "0.1.2.0/24",
    72  		ConfigType:       network.ConfigStatic,
    73  		NoAutoStart:      false,
    74  		Address:          network.NewAddress("0.1.2.3"),
    75  		DNSServers:       network.NewAddresses("ns1.invalid", "ns2.invalid"),
    76  		DNSSearchDomains: []string{"foo", "bar"},
    77  		GatewayAddress:   network.NewAddress("0.1.2.1"),
    78  		MACAddress:       "aa:bb:cc:dd:ee:f0",
    79  		MTU:              8317,
    80  	}, {
    81  		InterfaceName:    "any1",
    82  		CIDR:             "0.2.2.0/24",
    83  		ConfigType:       network.ConfigStatic,
    84  		NoAutoStart:      false,
    85  		Address:          network.NewAddress("0.2.2.4"),
    86  		DNSServers:       network.NewAddresses("ns1.invalid", "ns2.invalid"),
    87  		DNSSearchDomains: []string{"foo", "bar"},
    88  		GatewayAddress:   network.NewAddress("0.2.2.1"),
    89  		MACAddress:       "aa:bb:cc:dd:ee:f1",
    90  		Routes: []network.Route{{
    91  			DestinationCIDR: "0.5.6.0/24",
    92  			GatewayIP:       "0.2.2.1",
    93  			Metric:          50,
    94  		}},
    95  	}, {
    96  		InterfaceName: "any2",
    97  		ConfigType:    network.ConfigDHCP,
    98  		MACAddress:    "aa:bb:cc:dd:ee:f2",
    99  		NoAutoStart:   true,
   100  	}, {
   101  		InterfaceName: "any3",
   102  		ConfigType:    network.ConfigDHCP,
   103  		MACAddress:    "aa:bb:cc:dd:ee:f3",
   104  		NoAutoStart:   false,
   105  	}, {
   106  		InterfaceName: "any4",
   107  		ConfigType:    network.ConfigManual,
   108  		MACAddress:    "aa:bb:cc:dd:ee:f4",
   109  		NoAutoStart:   true,
   110  	}, {
   111  		InterfaceName:  "any5",
   112  		ConfigType:     network.ConfigStatic,
   113  		MACAddress:     "aa:bb:cc:dd:ee:f5",
   114  		NoAutoStart:    false,
   115  		CIDR:           "2001:db8::/64",
   116  		Address:        network.NewAddress("2001:db8::dead:beef"),
   117  		GatewayAddress: network.NewAddress("2001:db8::dead:f00"),
   118  	}}
   119  
   120  	for _, version := range []string{
   121  		"/usr/bin/python2",
   122  		"/usr/bin/python3",
   123  		"/usr/bin/python",
   124  	} {
   125  		if _, err := os.Stat(version); err == nil {
   126  			s.pythonVersions = append(s.pythonVersions, version)
   127  		}
   128  	}
   129  	c.Assert(s.pythonVersions, gc.Not(gc.HasLen), 0)
   130  
   131  	s.expectedSampleConfigHeader = `#cloud-config
   132  bootcmd:
   133  `
   134  	s.expectedSampleConfigWriting = `- install -D -m 644 /dev/null '%[1]s.templ'
   135  - |-
   136    printf '%%s\n' '
   137    auto lo {ethaa_bb_cc_dd_ee_f0} {ethaa_bb_cc_dd_ee_f1} {ethaa_bb_cc_dd_ee_f3} {ethaa_bb_cc_dd_ee_f5}
   138  
   139    iface lo inet loopback
   140      dns-nameservers ns1.invalid ns2.invalid
   141      dns-search bar foo
   142  
   143    iface {ethaa_bb_cc_dd_ee_f0} inet static
   144      address 0.1.2.3/24
   145      gateway 0.1.2.1
   146      mtu 8317
   147  
   148    iface {ethaa_bb_cc_dd_ee_f1} inet static
   149      address 0.2.2.4/24
   150      post-up ip route add 0.5.6.0/24 via 0.2.2.1 metric 50
   151      pre-down ip route del 0.5.6.0/24 via 0.2.2.1 metric 50
   152  
   153    iface {ethaa_bb_cc_dd_ee_f2} inet dhcp
   154  
   155    iface {ethaa_bb_cc_dd_ee_f3} inet dhcp
   156  
   157    iface {ethaa_bb_cc_dd_ee_f4} inet manual
   158  
   159    iface {ethaa_bb_cc_dd_ee_f5} inet6 static
   160      address 2001:db8::dead:beef/64
   161      gateway 2001:db8::dead:f00
   162    ' > '%[1]s.templ'
   163  `
   164  	s.expectedSampleConfigTemplate = `
   165  auto lo {ethaa_bb_cc_dd_ee_f0} {ethaa_bb_cc_dd_ee_f1} {ethaa_bb_cc_dd_ee_f3} {ethaa_bb_cc_dd_ee_f5}
   166  
   167  iface lo inet loopback
   168    dns-nameservers ns1.invalid ns2.invalid
   169    dns-search bar foo
   170  
   171  iface {ethaa_bb_cc_dd_ee_f0} inet static
   172    address 0.1.2.3/24
   173    gateway 0.1.2.1
   174    mtu 8317
   175  
   176  iface {ethaa_bb_cc_dd_ee_f1} inet static
   177    address 0.2.2.4/24
   178    post-up ip route add 0.5.6.0/24 via 0.2.2.1 metric 50
   179    pre-down ip route del 0.5.6.0/24 via 0.2.2.1 metric 50
   180  
   181  iface {ethaa_bb_cc_dd_ee_f2} inet dhcp
   182  
   183  iface {ethaa_bb_cc_dd_ee_f3} inet dhcp
   184  
   185  iface {ethaa_bb_cc_dd_ee_f4} inet manual
   186  
   187  iface {ethaa_bb_cc_dd_ee_f5} inet6 static
   188    address 2001:db8::dead:beef/64
   189    gateway 2001:db8::dead:f00
   190  `
   191  
   192  	networkInterfacesScriptYamled := strings.Replace(cloudinit.NetworkInterfacesScript, "\n", "\n  ", -1)
   193  	networkInterfacesScriptYamled = strings.Replace(networkInterfacesScriptYamled, "\n  \n", "\n\n", -1)
   194  	networkInterfacesScriptYamled = strings.Replace(networkInterfacesScriptYamled, "%", "%%", -1)
   195  	networkInterfacesScriptYamled = strings.Replace(networkInterfacesScriptYamled, "'", "'\"'\"'", -1)
   196  
   197  	s.expectedSampleUserData = `- install -D -m 744 /dev/null '%[2]s'
   198  - |-
   199    printf '%%s\n' '` + networkInterfacesScriptYamled + ` ' > '%[2]s'
   200  - |2
   201  
   202    if [ ! -f /sbin/ifup ]; then
   203        echo "No /sbin/ifup, applying netplan configuration."
   204        netplan generate
   205        netplan apply
   206    else
   207      if [ -f /usr/bin/python ]; then
   208          python %[1]s.py --interfaces-file %[1]s --output-file %[1]s.out
   209      else
   210          python3 %[1]s.py --interfaces-file %[1]s --output-file %[1]s.out
   211      fi
   212      ifdown -a
   213      sleep 1.5
   214      mv %[1]s.out %[1]s
   215      ifup -a
   216    fi
   217  `[1:]
   218  	s.expectedFullNetplanYaml = `
   219  - install -D -m 644 /dev/null '%[1]s'
   220  - |-
   221    printf '%%s\n' 'network:
   222      version: 2
   223      ethernets:
   224        any0:
   225          match:
   226            macaddress: aa:bb:cc:dd:ee:f0
   227          addresses:
   228          - 0.1.2.3/24
   229          gateway4: 0.1.2.1
   230          nameservers:
   231            search: [foo, bar]
   232            addresses: [ns1.invalid, ns2.invalid]
   233          mtu: 8317
   234        any1:
   235          match:
   236            macaddress: aa:bb:cc:dd:ee:f1
   237          addresses:
   238          - 0.2.2.4/24
   239          gateway4: 0.2.2.1
   240          nameservers:
   241            search: [foo, bar]
   242            addresses: [ns1.invalid, ns2.invalid]
   243          routes:
   244          - to: 0.5.6.0/24
   245            via: 0.2.2.1
   246            metric: 50
   247        any2:
   248          match:
   249            macaddress: aa:bb:cc:dd:ee:f2
   250          dhcp4: true
   251        any3:
   252          match:
   253            macaddress: aa:bb:cc:dd:ee:f3
   254          dhcp4: true
   255        any4:
   256          match:
   257            macaddress: aa:bb:cc:dd:ee:f4
   258        any5:
   259          match:
   260            macaddress: aa:bb:cc:dd:ee:f5
   261          addresses:
   262          - 2001:db8::dead:beef/64
   263          gateway6: 2001:db8::dead:f00
   264    ' > '%[1]s'
   265  `[1:]
   266  
   267  	s.expectedFullNetplan = `
   268  network:
   269    version: 2
   270    ethernets:
   271      any0:
   272        match:
   273          macaddress: aa:bb:cc:dd:ee:f0
   274        addresses:
   275        - 0.1.2.3/24
   276        gateway4: 0.1.2.1
   277        nameservers:
   278          search: [foo, bar]
   279          addresses: [ns1.invalid, ns2.invalid]
   280        mtu: 8317
   281      any1:
   282        match:
   283          macaddress: aa:bb:cc:dd:ee:f1
   284        addresses:
   285        - 0.2.2.4/24
   286        gateway4: 0.2.2.1
   287        nameservers:
   288          search: [foo, bar]
   289          addresses: [ns1.invalid, ns2.invalid]
   290        routes:
   291        - to: 0.5.6.0/24
   292          via: 0.2.2.1
   293          metric: 50
   294      any2:
   295        match:
   296          macaddress: aa:bb:cc:dd:ee:f2
   297        dhcp4: true
   298      any3:
   299        match:
   300          macaddress: aa:bb:cc:dd:ee:f3
   301        dhcp4: true
   302      any4:
   303        match:
   304          macaddress: aa:bb:cc:dd:ee:f4
   305      any5:
   306        match:
   307          macaddress: aa:bb:cc:dd:ee:f5
   308        addresses:
   309        - 2001:db8::dead:beef/64
   310        gateway6: 2001:db8::dead:f00
   311  `[1:]
   312  
   313  	s.expectedBaseNetplanYaml = `
   314  - install -D -m 644 /dev/null '%[1]s'
   315  - |-
   316    printf '%%s\n' 'network:
   317      version: 2
   318      ethernets:
   319        eth0:
   320          match:
   321            name: eth0
   322          dhcp4: true
   323    ' > '%[1]s'
   324  `[1:]
   325  
   326  	s.expectedFallbackConfig = `- install -D -m 644 /dev/null '%[1]s.templ'
   327  - |-
   328    printf '%%s\n' '
   329    auto lo {eth}
   330  
   331    iface lo inet loopback
   332  
   333    iface {eth} inet dhcp
   334    ' > '%[1]s.templ'
   335  `
   336  
   337  	s.expectedBaseConfig = `
   338  auto lo {eth}
   339  
   340  iface lo inet loopback
   341  
   342  iface {eth} inet dhcp
   343  `
   344  
   345  	s.expectedBaseNetplan = `
   346  network:
   347    version: 2
   348    ethernets:
   349      eth0:
   350        match:
   351          name: eth0
   352        dhcp4: true
   353  `[1:]
   354  
   355  	s.expectedFallbackUserData = `
   356  #cloud-config
   357  bootcmd:
   358  - install -D -m 644 /dev/null '%[1]s'
   359  - |-
   360    printf '%%s\n' '
   361    auto eth0 lo
   362  
   363    iface lo inet loopback
   364  
   365    iface eth0 inet dhcp
   366    ' > '%[1]s'
   367  runcmd:
   368  - |-
   369    if [ -f %[1]s ]; then
   370        echo "stopping all interfaces"
   371        ifdown -a
   372        sleep 1.5
   373        if ifup -a --interfaces=%[1]s; then
   374            echo "ifup with %[1]s succeeded, renaming to %[2]s"
   375            cp %[2]s %[2]s-orig
   376            cp %[1]s %[2]s
   377        else
   378            echo "ifup with %[1]s failed, leaving old %[2]s alone"
   379            ifup -a
   380        fi
   381    else
   382        echo "did not find %[1]s, not reconfiguring networking"
   383    fi
   384  `[1:]
   385  
   386  	s.originalSystemNetworkInterfaces = `
   387  # This file describes the network interfaces available on your system
   388  # and how to activate them. For more information, see interfaces(5).
   389  
   390  # The loopback network interface
   391  auto lo
   392  iface lo inet loopback
   393  
   394  # Source interfaces
   395  # Please check /etc/network/interfaces.d before changing this file
   396  # as interfaces may have been defined in /etc/network/interfaces.d
   397  # See LP: #1262951
   398  source /etc/network/interfaces.d/*.cfg
   399  `[1:]
   400  
   401  	s.PatchValue(cloudinit.NetworkInterfacesFile, s.systemNetworkInterfacesFile)
   402  	s.PatchValue(cloudinit.SystemNetworkInterfacesFile, s.systemNetworkInterfacesFile)
   403  	s.PatchValue(cloudinit.JujuNetplanFile, s.jujuNetplanFile)
   404  }
   405  
   406  func (s *NetworkUbuntuSuite) TestGenerateENIConfig(c *gc.C) {
   407  	data, err := cloudinit.GenerateENITemplate(nil)
   408  	c.Assert(err, gc.ErrorMatches, "missing container network config")
   409  	c.Assert(data, gc.Equals, "")
   410  
   411  	netConfig := container.BridgeNetworkConfig("foo", 0, nil)
   412  	data, err = cloudinit.GenerateENITemplate(netConfig.Interfaces)
   413  	c.Assert(err, jc.ErrorIsNil)
   414  	c.Check(data, gc.Equals, s.expectedBaseConfig)
   415  
   416  	// Test with all interface types.
   417  	netConfig = container.BridgeNetworkConfig("foo", 0, s.fakeInterfaces)
   418  	data, err = cloudinit.GenerateENITemplate(netConfig.Interfaces)
   419  	c.Assert(err, jc.ErrorIsNil)
   420  	c.Check(data, gc.Equals, s.expectedSampleConfigTemplate)
   421  }
   422  
   423  func (s *NetworkUbuntuSuite) TestGenerateNetplan(c *gc.C) {
   424  	data, err := cloudinit.GenerateNetplan(nil)
   425  	c.Assert(err, gc.ErrorMatches, "missing container network config")
   426  	c.Assert(data, gc.Equals, "")
   427  
   428  	netConfig := container.BridgeNetworkConfig("foo", 0, nil)
   429  	data, err = cloudinit.GenerateNetplan(netConfig.Interfaces)
   430  	c.Assert(err, jc.ErrorIsNil)
   431  	c.Check(data, gc.Equals, s.expectedBaseNetplan)
   432  
   433  	// Test with all interface types.
   434  	netConfig = container.BridgeNetworkConfig("foo", 0, s.fakeInterfaces)
   435  	data, err = cloudinit.GenerateNetplan(netConfig.Interfaces)
   436  	c.Assert(err, jc.ErrorIsNil)
   437  	c.Check(data, gc.Equals, s.expectedFullNetplan)
   438  }
   439  
   440  func (s *NetworkUbuntuSuite) TestAddNetworkConfigSampleConfig(c *gc.C) {
   441  	netConfig := container.BridgeNetworkConfig("foo", 0, s.fakeInterfaces)
   442  	cloudConf, err := cloudinit.New("xenial")
   443  	c.Assert(err, jc.ErrorIsNil)
   444  	c.Assert(cloudConf, gc.NotNil)
   445  	err = cloudConf.AddNetworkConfig(netConfig.Interfaces)
   446  	c.Assert(err, jc.ErrorIsNil)
   447  
   448  	expected := s.expectedSampleConfigHeader
   449  	expected += fmt.Sprintf(s.expectedFullNetplanYaml, s.jujuNetplanFile)
   450  	expected += fmt.Sprintf(s.expectedSampleConfigWriting, s.systemNetworkInterfacesFile)
   451  	expected += fmt.Sprintf(s.expectedSampleUserData, s.systemNetworkInterfacesFile, s.networkInterfacesPythonFile, s.systemNetworkInterfacesFile)
   452  	assertUserData(c, cloudConf, expected)
   453  }
   454  
   455  func (s *NetworkUbuntuSuite) TestAddNetworkConfigFallbackConfig(c *gc.C) {
   456  	netConfig := container.BridgeNetworkConfig("foo", 0, nil)
   457  	cloudConf, err := cloudinit.New("xenial")
   458  	c.Assert(err, jc.ErrorIsNil)
   459  	c.Assert(cloudConf, gc.NotNil)
   460  	err = cloudConf.AddNetworkConfig(netConfig.Interfaces)
   461  	c.Assert(err, jc.ErrorIsNil)
   462  	c.Assert(cloudConf, gc.NotNil)
   463  	expected := s.expectedSampleConfigHeader
   464  	expected += fmt.Sprintf(s.expectedBaseNetplanYaml, s.jujuNetplanFile)
   465  	expected += fmt.Sprintf(s.expectedFallbackConfig, s.systemNetworkInterfacesFile, s.systemNetworkInterfacesFile)
   466  	expected += fmt.Sprintf(s.expectedSampleUserData, s.systemNetworkInterfacesFile, s.networkInterfacesPythonFile)
   467  	assertUserData(c, cloudConf, expected)
   468  }
   469  
   470  func CloudInitDataExcludingOutputSection(data string) []string {
   471  	// Extract the "#cloud-config" header and all lines between
   472  	// from the "bootcmd" section up to (but not including) the
   473  	// "output" sections to match against expected. But we cannot
   474  	// possibly handle all the /other/ output that may be added by
   475  	// CloudInitUserData() in the future, so we also truncate at
   476  	// the first runcmd which now happens to include the runcmd's
   477  	// added for raising the network interfaces captured in
   478  	// expectedFallbackUserData. However, the other tests above do
   479  	// check for that output.
   480  
   481  	var linesToMatch []string
   482  	seenBootcmd := false
   483  	for _, line := range strings.Split(string(data), "\n") {
   484  		if strings.HasPrefix(line, "#cloud-config") {
   485  			linesToMatch = append(linesToMatch, line)
   486  			continue
   487  		}
   488  
   489  		if strings.HasPrefix(line, "bootcmd:") {
   490  			seenBootcmd = true
   491  		}
   492  
   493  		if strings.HasPrefix(line, "output:") && seenBootcmd {
   494  			break
   495  		}
   496  
   497  		if seenBootcmd {
   498  			linesToMatch = append(linesToMatch, line)
   499  		}
   500  	}
   501  
   502  	return linesToMatch
   503  }
   504  
   505  func assertUserData(c *gc.C, cloudConf cloudinit.CloudConfig, expected string) {
   506  	data, err := cloudConf.RenderYAML()
   507  	c.Assert(err, jc.ErrorIsNil)
   508  	c.Assert(string(data), gc.Equals, expected)
   509  
   510  	// Make sure it's valid YAML as well.
   511  	out := make(map[string]interface{})
   512  	err = yaml.Unmarshal(data, &out)
   513  	c.Assert(err, jc.ErrorIsNil)
   514  	if len(cloudConf.BootCmds()) > 0 {
   515  		outcmds := out["bootcmd"].([]interface{})
   516  		confcmds := cloudConf.BootCmds()
   517  		c.Assert(len(outcmds), gc.Equals, len(confcmds))
   518  		for i := range outcmds {
   519  			c.Assert(outcmds[i].(string), gc.Equals, confcmds[i])
   520  		}
   521  	} else {
   522  		c.Assert(out["bootcmd"], gc.IsNil)
   523  	}
   524  }
   525  
   526  func (s *NetworkUbuntuSuite) TestENIScriptSimple(c *gc.C) {
   527  	cmd := s.createMockCommand(c, []string{simpleENIFileIPOutput})
   528  	s.runENIScriptWithAllPythons(c, cmd, simpleENIFile, simpleENIFileExpected, 0, 1)
   529  }
   530  
   531  func (s *NetworkUbuntuSuite) TestENIScriptUnknownMAC(c *gc.C) {
   532  	cmd := s.createMockCommand(c, []string{unknownMACENIFileIPOutput})
   533  	s.runENIScriptWithAllPythons(c, cmd, unknownMACENIFile, unknownMACENIFileExpected, 0, 1)
   534  }
   535  
   536  func (s *NetworkUbuntuSuite) TestENIScriptHotplugFail(c *gc.C) {
   537  	cmd := s.createMockCommand(c, []string{hotplugENIFileIPOutputPre})
   538  	s.runENIScriptWithAllPythons(c, cmd, hotplugENIFile, hotplugENIFileExpectedFail, 0, 3)
   539  }
   540  
   541  func (s *NetworkUbuntuSuite) TestENIScriptHotplugTooLate(c *gc.C) {
   542  	for _, python := range s.pythonVersions {
   543  		c.Logf("test using %s", python)
   544  		ipCommand := s.createMockCommand(c, []string{hotplugENIFileIPOutputPre, hotplugENIFileIPOutputPre, hotplugENIFileIPOutputPre, hotplugENIFileIPOutputPost})
   545  		s.runENIScript(c, python, ipCommand, hotplugENIFile, hotplugENIFileExpectedFail, 0, 3)
   546  	}
   547  }
   548  
   549  func (s *NetworkUbuntuSuite) TestENIScriptHotplug(c *gc.C) {
   550  	for _, python := range s.pythonVersions {
   551  		c.Logf("test using %s", python)
   552  		ipCommand := s.createMockCommand(c, []string{hotplugENIFileIPOutputPre, hotplugENIFileIPOutputPre, hotplugENIFileIPOutputPost})
   553  		s.runENIScript(c, python, ipCommand, hotplugENIFile, hotplugENIFileExpected, 0, 3)
   554  	}
   555  }
   556  
   557  func (s *NetworkUbuntuSuite) runENIScriptWithAllPythons(c *gc.C, ipCommand, input, expectedOutput string, wait, retries int) {
   558  	for _, python := range s.pythonVersions {
   559  		c.Logf("test using %s", python)
   560  		s.runENIScript(c, python, ipCommand, input, expectedOutput, wait, retries)
   561  	}
   562  
   563  }
   564  
   565  func (s *NetworkUbuntuSuite) runENIScript(c *gc.C, pythonBinary, ipCommand, input, expectedOutput string, wait, retries int) {
   566  	dataFile := filepath.Join(s.tempFolder, "interfaces")
   567  	dataOutFile := filepath.Join(s.tempFolder, "interfaces.out")
   568  	dataBakFile := filepath.Join(s.tempFolder, "interfaces.bak")
   569  	templFile := filepath.Join(s.tempFolder, "interfaces.templ")
   570  	scriptFile := filepath.Join(s.tempFolder, "script.py")
   571  
   572  	err := ioutil.WriteFile(dataFile, []byte(s.originalSystemNetworkInterfaces), 0644)
   573  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("Can't write interfaces file"))
   574  
   575  	err = ioutil.WriteFile(templFile, []byte(input), 0644)
   576  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("Can't write interfaces.templ file"))
   577  
   578  	err = ioutil.WriteFile(scriptFile, []byte(cloudinit.NetworkInterfacesScript), 0755)
   579  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("Can't write script file"))
   580  
   581  	script := fmt.Sprintf("%q %q --interfaces-file %q --output-file %q --command %q --wait %d --retries %d",
   582  		pythonBinary, scriptFile, dataFile, dataOutFile, ipCommand, wait, retries)
   583  	result, err := exec.RunCommands(exec.RunParams{Commands: script})
   584  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("script failed unexpectedly - %s", result))
   585  	c.Logf("%s\n%s\n", string(result.Stdout), string(result.Stderr))
   586  
   587  	for file, expected := range map[string]string{
   588  		dataBakFile: s.originalSystemNetworkInterfaces,
   589  		dataOutFile: expectedOutput,
   590  		dataFile:    s.originalSystemNetworkInterfaces,
   591  	} {
   592  		data, err := ioutil.ReadFile(file)
   593  		c.Assert(err, jc.ErrorIsNil, gc.Commentf("can't open %q file: %s", file, err))
   594  		output := string(data)
   595  		c.Assert(output, gc.Equals, expected)
   596  	}
   597  }
   598  
   599  func (s *NetworkUbuntuSuite) createMockCommand(c *gc.C, outputs []string) string {
   600  	randBytes := make([]byte, 16)
   601  	rand.Read(randBytes)
   602  	baseName := hex.EncodeToString(randBytes)
   603  	basePath := filepath.Join(s.tempFolder, fmt.Sprintf("%s.%d", baseName, 0))
   604  	script := fmt.Sprintf("#!/bin/bash\ncat %s\n", basePath)
   605  
   606  	lastFile := ""
   607  	for i, output := range outputs {
   608  		dataFile := filepath.Join(s.tempFolder, fmt.Sprintf("%s.%d", baseName, i))
   609  		err := ioutil.WriteFile(dataFile, []byte(output), 0644)
   610  		c.Assert(err, jc.ErrorIsNil, gc.Commentf("can't write mock file"))
   611  		if lastFile != "" {
   612  			script += fmt.Sprintf("mv %q %q || true\n", dataFile, lastFile)
   613  		}
   614  		lastFile = dataFile
   615  	}
   616  
   617  	scriptPath := filepath.Join(s.tempFolder, fmt.Sprintf("%s.sh", baseName))
   618  	err := ioutil.WriteFile(scriptPath, []byte(script), 0755)
   619  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("can't write script file"))
   620  	return scriptPath
   621  }
   622  
   623  const simpleENIFile = `auto lo
   624  interface lo inet loopback
   625  
   626  auto {ethe0_db_55_e4_1d_5b}
   627  iface {ethe0_db_55_e4_1d_5b} inet static
   628      address 1.2.3.4
   629      netmask 255.255.255.0
   630      gateway 1.2.3.1
   631  
   632  auto {ethe0_db_55_e4_1a_5b}
   633  iface {ethe0_db_55_e4_1a_5b} inet static
   634      address 1.2.3.5
   635      netmask 255.255.255.0
   636      gateway 1.2.3.1
   637  
   638  auto {ethe0_db_55_e4_1a_5d}
   639  iface {ethe0_db_55_e4_1a_5d} inet static
   640      address 1.2.3.6
   641      netmask 255.255.255.0
   642      gateway 1.2.3.1
   643  `
   644  
   645  const simpleENIFileIPOutput = `1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1\    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   646  2: eno1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000\    link/ether e0:db:55:e4:1d:5b brd ff:ff:ff:ff:ff:ff
   647  3: eno2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000\    link/ether e0:db:55:e4:1a:5b brd ff:ff:ff:ff:ff:ff
   648  3: eno3@if0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000\    link/ether e0:db:55:e4:1a:5d brd ff:ff:ff:ff:ff:ff
   649  `
   650  
   651  const simpleENIFileExpected = `auto lo
   652  interface lo inet loopback
   653  
   654  auto eno1
   655  iface eno1 inet static
   656      address 1.2.3.4
   657      netmask 255.255.255.0
   658      gateway 1.2.3.1
   659  
   660  auto eno2
   661  iface eno2 inet static
   662      address 1.2.3.5
   663      netmask 255.255.255.0
   664      gateway 1.2.3.1
   665  
   666  auto eno3
   667  iface eno3 inet static
   668      address 1.2.3.6
   669      netmask 255.255.255.0
   670      gateway 1.2.3.1
   671  `
   672  
   673  const unknownMACENIFile = `auto lo
   674  interface lo inet loopback
   675  
   676  auto {ethe0_db_55_e4_1d_5b}
   677  iface {ethe0_db_55_e4_1d_5b} inet static
   678      address 1.2.3.4
   679      netmask 255.255.255.0
   680      gateway 1.2.3.1
   681  
   682  auto {ethe3_db_55_e4_1d_5b}
   683  iface {ethe3_db_55_e4_1d_5b} inet static
   684      address 1.2.3.5
   685      netmask 255.255.255.0
   686      gateway 1.2.3.1
   687  `
   688  
   689  const unknownMACENIFileIPOutput = `1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1\    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   690  2: eno1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000\    link/ether e0:db:55:e4:1d:5b brd ff:ff:ff:ff:ff:ff
   691  `
   692  
   693  const unknownMACENIFileExpected = `auto lo
   694  interface lo inet loopback
   695  
   696  auto eno1
   697  iface eno1 inet static
   698      address 1.2.3.4
   699      netmask 255.255.255.0
   700      gateway 1.2.3.1
   701  
   702  auto ethe3_db_55_e4_1d_5b
   703  iface ethe3_db_55_e4_1d_5b inet static
   704      address 1.2.3.5
   705      netmask 255.255.255.0
   706      gateway 1.2.3.1
   707  `
   708  
   709  const hotplugENIFile = `auto lo
   710  interface lo inet loopback
   711  
   712  auto {ethe0_db_55_e4_1d_5b}
   713  iface {ethe0_db_55_e4_1d_5b} inet static
   714      address 1.2.3.4
   715      netmask 255.255.255.0
   716      gateway 1.2.3.1
   717  
   718  auto {ethe0_db_55_e4_1a_5b}
   719  iface {ethe0_db_55_e4_1a_5b} inet static
   720      address 1.2.3.5
   721      netmask 255.255.255.0
   722      gateway 1.2.3.1
   723  `
   724  
   725  const hotplugENIFileIPOutputPre = `1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1\    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   726  2: eno1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000\    link/ether e0:db:55:e4:1d:5b brd ff:ff:ff:ff:ff:ff
   727  `
   728  
   729  const hotplugENIFileIPOutputPost = `1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1\    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   730  2: eno1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000\    link/ether e0:db:55:e4:1d:5b brd ff:ff:ff:ff:ff:ff
   731  32: eno2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000\    link/ether e0:db:55:e4:1a:5b brd ff:ff:ff:ff:ff:ff
   732  `
   733  
   734  const hotplugENIFileExpected = `auto lo
   735  interface lo inet loopback
   736  
   737  auto eno1
   738  iface eno1 inet static
   739      address 1.2.3.4
   740      netmask 255.255.255.0
   741      gateway 1.2.3.1
   742  
   743  auto eno2
   744  iface eno2 inet static
   745      address 1.2.3.5
   746      netmask 255.255.255.0
   747      gateway 1.2.3.1
   748  `
   749  
   750  const hotplugENIFileExpectedFail = `auto lo
   751  interface lo inet loopback
   752  
   753  auto eno1
   754  iface eno1 inet static
   755      address 1.2.3.4
   756      netmask 255.255.255.0
   757      gateway 1.2.3.1
   758  
   759  auto ethe0_db_55_e4_1a_5b
   760  iface ethe0_db_55_e4_1a_5b inet static
   761      address 1.2.3.5
   762      netmask 255.255.255.0
   763      gateway 1.2.3.1
   764  `