github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/maas/constraints_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package maas
     5  
     6  import (
     7  	"net/url"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/gomaasapi"
    11  	jc "github.com/juju/testing/checkers"
    12  	gc "gopkg.in/check.v1"
    13  
    14  	"github.com/juju/juju/core/constraints"
    15  )
    16  
    17  func (*environSuite) TestConvertConstraints(c *gc.C) {
    18  	for i, test := range []struct {
    19  		cons     constraints.Value
    20  		expected url.Values
    21  	}{{
    22  		cons:     constraints.Value{Arch: stringp("arm")},
    23  		expected: url.Values{"arch": {"arm"}},
    24  	}, {
    25  		cons:     constraints.Value{CpuCores: uint64p(4)},
    26  		expected: url.Values{"cpu_count": {"4"}},
    27  	}, {
    28  		cons:     constraints.Value{Mem: uint64p(1024)},
    29  		expected: url.Values{"mem": {"1024"}},
    30  	}, { // Spaces are converted to bindings and not_networks, but only in acquireNode
    31  		cons:     constraints.Value{Spaces: stringslicep("foo", "bar", "^baz", "^oof")},
    32  		expected: url.Values{},
    33  	}, {
    34  		cons: constraints.Value{Tags: stringslicep("tag1", "tag2", "^tag3", "^tag4")},
    35  		expected: url.Values{
    36  			"tags":     {"tag1,tag2"},
    37  			"not_tags": {"tag3,tag4"},
    38  		},
    39  	}, { // CpuPower is ignored.
    40  		cons:     constraints.Value{CpuPower: uint64p(1024)},
    41  		expected: url.Values{},
    42  	}, { // RootDisk is ignored.
    43  		cons:     constraints.Value{RootDisk: uint64p(8192)},
    44  		expected: url.Values{},
    45  	}, {
    46  		cons:     constraints.Value{Tags: stringslicep("foo", "bar")},
    47  		expected: url.Values{"tags": {"foo,bar"}},
    48  	}, {
    49  		cons: constraints.Value{
    50  			Arch:     stringp("arm"),
    51  			CpuCores: uint64p(4),
    52  			Mem:      uint64p(1024),
    53  			CpuPower: uint64p(1024),
    54  			RootDisk: uint64p(8192),
    55  			Spaces:   stringslicep("foo", "^bar"),
    56  			Tags:     stringslicep("^tag1", "tag2"),
    57  		},
    58  		expected: url.Values{
    59  			"arch":      {"arm"},
    60  			"cpu_count": {"4"},
    61  			"mem":       {"1024"},
    62  			"tags":      {"tag2"},
    63  			"not_tags":  {"tag1"},
    64  		},
    65  	}} {
    66  		c.Logf("test #%d: cons=%s", i, test.cons.String())
    67  		c.Check(convertConstraints(test.cons), jc.DeepEquals, test.expected)
    68  	}
    69  }
    70  
    71  func (*environSuite) TestConvertConstraints2(c *gc.C) {
    72  	for i, test := range []struct {
    73  		cons     constraints.Value
    74  		expected gomaasapi.AllocateMachineArgs
    75  	}{{
    76  		cons:     constraints.Value{Arch: stringp("arm")},
    77  		expected: gomaasapi.AllocateMachineArgs{Architecture: "arm"},
    78  	}, {
    79  		cons:     constraints.Value{CpuCores: uint64p(4)},
    80  		expected: gomaasapi.AllocateMachineArgs{MinCPUCount: 4},
    81  	}, {
    82  		cons:     constraints.Value{Mem: uint64p(1024)},
    83  		expected: gomaasapi.AllocateMachineArgs{MinMemory: 1024},
    84  	}, { // Spaces are converted to bindings and not_networks, but only in acquireNode
    85  		cons:     constraints.Value{Spaces: stringslicep("foo", "bar", "^baz", "^oof")},
    86  		expected: gomaasapi.AllocateMachineArgs{},
    87  	}, {
    88  		cons: constraints.Value{Tags: stringslicep("tag1", "tag2", "^tag3", "^tag4")},
    89  		expected: gomaasapi.AllocateMachineArgs{
    90  			Tags:    []string{"tag1", "tag2"},
    91  			NotTags: []string{"tag3", "tag4"},
    92  		},
    93  	}, { // CpuPower is ignored.
    94  		cons:     constraints.Value{CpuPower: uint64p(1024)},
    95  		expected: gomaasapi.AllocateMachineArgs{},
    96  	}, { // RootDisk is ignored.
    97  		cons:     constraints.Value{RootDisk: uint64p(8192)},
    98  		expected: gomaasapi.AllocateMachineArgs{},
    99  	}, {
   100  		cons: constraints.Value{Tags: stringslicep("foo", "bar")},
   101  		expected: gomaasapi.AllocateMachineArgs{
   102  			Tags: []string{"foo", "bar"},
   103  		},
   104  	}, {
   105  		cons: constraints.Value{
   106  			Arch:     stringp("arm"),
   107  			CpuCores: uint64p(4),
   108  			Mem:      uint64p(1024),
   109  			CpuPower: uint64p(1024),
   110  			RootDisk: uint64p(8192),
   111  			Spaces:   stringslicep("foo", "^bar"),
   112  			Tags:     stringslicep("^tag1", "tag2"),
   113  		},
   114  		expected: gomaasapi.AllocateMachineArgs{
   115  			Architecture: "arm",
   116  			MinCPUCount:  4,
   117  			MinMemory:    1024,
   118  			Tags:         []string{"tag2"},
   119  			NotTags:      []string{"tag1"},
   120  		},
   121  	}} {
   122  		c.Logf("test #%d: cons2=%s", i, test.cons.String())
   123  		c.Check(convertConstraints2(test.cons), jc.DeepEquals, test.expected)
   124  	}
   125  }
   126  
   127  var nilStringSlice []string
   128  
   129  func (*environSuite) TestConvertTagsToParams(c *gc.C) {
   130  	for i, test := range []struct {
   131  		tags     *[]string
   132  		expected url.Values
   133  	}{{
   134  		tags:     nil,
   135  		expected: url.Values{},
   136  	}, {
   137  		tags:     &nilStringSlice,
   138  		expected: url.Values{},
   139  	}, {
   140  		tags:     &[]string{},
   141  		expected: url.Values{},
   142  	}, {
   143  		tags:     stringslicep(""),
   144  		expected: url.Values{},
   145  	}, {
   146  		tags: stringslicep("foo"),
   147  		expected: url.Values{
   148  			"tags": {"foo"},
   149  		},
   150  	}, {
   151  		tags: stringslicep("^bar"),
   152  		expected: url.Values{
   153  			"not_tags": {"bar"},
   154  		},
   155  	}, {
   156  		tags: stringslicep("foo", "^bar", "baz", "^oof"),
   157  		expected: url.Values{
   158  			"tags":     {"foo,baz"},
   159  			"not_tags": {"bar,oof"},
   160  		},
   161  	}, {
   162  		tags: stringslicep("", "^bar", "^", "^oof"),
   163  		expected: url.Values{
   164  			"not_tags": {"bar,oof"},
   165  		},
   166  	}, {
   167  		tags: stringslicep("foo", "^", " b a z  ", "^^ ^"),
   168  		expected: url.Values{
   169  			"tags":     {"foo, b a z  "},
   170  			"not_tags": {"^ ^"},
   171  		},
   172  	}, {
   173  		tags: stringslicep("", "^bar", "  ", " ^ o of "),
   174  		expected: url.Values{
   175  			"tags":     {"  , ^ o of "},
   176  			"not_tags": {"bar"},
   177  		},
   178  	}, {
   179  		tags: stringslicep("foo", "foo", "^bar", "^bar"),
   180  		expected: url.Values{
   181  			"tags":     {"foo,foo"},
   182  			"not_tags": {"bar,bar"},
   183  		},
   184  	}} {
   185  		c.Logf("test #%d: tags=%v", i, test.tags)
   186  		var vals = url.Values{}
   187  		convertTagsToParams(vals, test.tags)
   188  		c.Check(vals, jc.DeepEquals, test.expected)
   189  	}
   190  }
   191  
   192  func uint64p(val uint64) *uint64 {
   193  	return &val
   194  }
   195  
   196  func stringp(val string) *string {
   197  	return &val
   198  }
   199  
   200  func stringslicep(values ...string) *[]string {
   201  	return &values
   202  }
   203  
   204  func (suite *environSuite) TestSelectNodeValidZone(c *gc.C) {
   205  	env := suite.makeEnviron()
   206  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0", "zone": "bar"}`)
   207  
   208  	snArgs := selectNodeArgs{
   209  		AvailabilityZone: "bar",
   210  		Constraints:      constraints.Value{},
   211  	}
   212  
   213  	node, err := env.selectNode(suite.callCtx, snArgs)
   214  	c.Assert(err, gc.IsNil)
   215  	c.Assert(node, gc.NotNil)
   216  }
   217  
   218  func (suite *environSuite) TestSelectNodeInvalidZone(c *gc.C) {
   219  	env := suite.makeEnviron()
   220  
   221  	snArgs := selectNodeArgs{
   222  		AvailabilityZone: "foo",
   223  		Constraints:      constraints.Value{},
   224  	}
   225  
   226  	_, err := env.selectNode(suite.callCtx, snArgs)
   227  	c.Assert(err, gc.NotNil)
   228  	c.Assert(err.noMatch, jc.IsTrue)
   229  	c.Assert(err, gc.ErrorMatches, ".*409.*")
   230  }
   231  
   232  func (suite *environSuite) TestAcquireNode(c *gc.C) {
   233  	env := suite.makeEnviron()
   234  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   235  
   236  	_, err := env.acquireNode(suite.callCtx, "", "", "", constraints.Value{}, nil, nil)
   237  
   238  	c.Check(err, jc.ErrorIsNil)
   239  	operations := suite.testMAASObject.TestServer.NodeOperations()
   240  	actions, found := operations["node0"]
   241  	c.Assert(found, jc.IsTrue)
   242  	c.Check(actions, gc.DeepEquals, []string{"acquire"})
   243  
   244  	// no "name" parameter should have been passed through
   245  	values := suite.testMAASObject.TestServer.NodeOperationRequestValues()["node0"][0]
   246  	_, found = values["name"]
   247  	c.Assert(found, jc.IsFalse)
   248  }
   249  
   250  func (suite *environSuite) TestAcquireNodeByName(c *gc.C) {
   251  	env := suite.makeEnviron()
   252  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   253  
   254  	_, err := env.acquireNode(suite.callCtx, "host0", "", "", constraints.Value{}, nil, nil)
   255  
   256  	c.Check(err, jc.ErrorIsNil)
   257  	operations := suite.testMAASObject.TestServer.NodeOperations()
   258  	actions, found := operations["node0"]
   259  	c.Assert(found, jc.IsTrue)
   260  	c.Check(actions, gc.DeepEquals, []string{"acquire"})
   261  
   262  	// no "name" parameter should have been passed through
   263  	values := suite.testMAASObject.TestServer.NodeOperationRequestValues()["node0"][0]
   264  	nodeName := values.Get("name")
   265  	c.Assert(nodeName, gc.Equals, "host0")
   266  }
   267  
   268  func (suite *environSuite) TestAcquireNodeTakesConstraintsIntoAccount(c *gc.C) {
   269  	env := suite.makeEnviron()
   270  	suite.testMAASObject.TestServer.NewNode(
   271  		`{"system_id": "node0", "hostname": "host0", "architecture": "arm/generic", "memory": 2048}`,
   272  	)
   273  	constraints := constraints.Value{Arch: stringp("arm"), Mem: uint64p(1024)}
   274  
   275  	_, err := env.acquireNode(suite.callCtx, "", "", "", constraints, nil, nil)
   276  
   277  	c.Check(err, jc.ErrorIsNil)
   278  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   279  	nodeRequestValues, found := requestValues["node0"]
   280  	c.Assert(found, jc.IsTrue)
   281  	c.Assert(nodeRequestValues[0].Get("arch"), gc.Equals, "arm")
   282  	c.Assert(nodeRequestValues[0].Get("mem"), gc.Equals, "1024")
   283  }
   284  
   285  func (suite *environSuite) TestParseDelimitedValues(c *gc.C) {
   286  	for i, test := range []struct {
   287  		about     string
   288  		input     []string
   289  		positives []string
   290  		negatives []string
   291  	}{{
   292  		about:     "nil input",
   293  		input:     nil,
   294  		positives: []string{},
   295  		negatives: []string{},
   296  	}, {
   297  		about:     "empty input",
   298  		input:     []string{},
   299  		positives: []string{},
   300  		negatives: []string{},
   301  	}, {
   302  		about:     "values list with embedded whitespace",
   303  		input:     []string{"   val1  ", " val2", " ^ not Val 3  ", "  ", " ", "^", "", "^ notVal4   "},
   304  		positives: []string{"   val1  ", " val2", " ^ not Val 3  ", "  ", " "},
   305  		negatives: []string{" notVal4   "},
   306  	}, {
   307  		about:     "only positives",
   308  		input:     []string{"val1", "val2", "val3"},
   309  		positives: []string{"val1", "val2", "val3"},
   310  		negatives: []string{},
   311  	}, {
   312  		about:     "only negatives",
   313  		input:     []string{"^val1", "^val2", "^val3"},
   314  		positives: []string{},
   315  		negatives: []string{"val1", "val2", "val3"},
   316  	}, {
   317  		about:     "multi-caret negatives",
   318  		input:     []string{"^foo^", "^v^a^l2", "  ^^ ^", "^v^al3", "^^", "^"},
   319  		positives: []string{"  ^^ ^"},
   320  		negatives: []string{"foo^", "v^a^l2", "v^al3", "^"},
   321  	}, {
   322  		about:     "both positives and negatives",
   323  		input:     []string{"^val1", "val2", "^val3", "val4"},
   324  		positives: []string{"val2", "val4"},
   325  		negatives: []string{"val1", "val3"},
   326  	}, {
   327  		about:     "single positive value",
   328  		input:     []string{"val1"},
   329  		positives: []string{"val1"},
   330  		negatives: []string{},
   331  	}, {
   332  		about:     "single negative value",
   333  		input:     []string{"^val1"},
   334  		positives: []string{},
   335  		negatives: []string{"val1"},
   336  	}} {
   337  		c.Logf("test %d: %s", i, test.about)
   338  		positives, negatives := parseDelimitedValues(test.input)
   339  		c.Check(positives, jc.DeepEquals, test.positives)
   340  		c.Check(negatives, jc.DeepEquals, test.negatives)
   341  	}
   342  }
   343  
   344  func (suite *environSuite) TestAcquireNodePassedAgentName(c *gc.C) {
   345  	env := suite.makeEnviron()
   346  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   347  
   348  	_, err := env.acquireNode(suite.callCtx, "", "", "", constraints.Value{}, nil, nil)
   349  
   350  	c.Check(err, jc.ErrorIsNil)
   351  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   352  	nodeRequestValues, found := requestValues["node0"]
   353  	c.Assert(found, jc.IsTrue)
   354  	c.Assert(nodeRequestValues[0].Get("agent_name"), gc.Equals, env.Config().UUID())
   355  }
   356  
   357  func (suite *environSuite) TestAcquireNodePassesPositiveAndNegativeTags(c *gc.C) {
   358  	env := suite.makeEnviron()
   359  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "tag_names": "tag1,tag3"}`)
   360  
   361  	_, err := env.acquireNode(
   362  		suite.callCtx,
   363  		"", "", "",
   364  		constraints.Value{Tags: stringslicep("tag1", "^tag2", "tag3", "^tag4")},
   365  		nil, nil,
   366  	)
   367  
   368  	c.Check(err, jc.ErrorIsNil)
   369  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   370  	nodeValues, found := requestValues["node0"]
   371  	c.Assert(found, jc.IsTrue)
   372  	c.Assert(nodeValues[0].Get("tags"), gc.Equals, "tag1,tag3")
   373  	c.Assert(nodeValues[0].Get("not_tags"), gc.Equals, "tag2,tag4")
   374  }
   375  
   376  func (suite *environSuite) TestAcquireNodePassesPositiveAndNegativeSpaces(c *gc.C) {
   377  	suite.createFourSpaces(c)
   378  	env := suite.makeEnviron()
   379  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0"}`)
   380  
   381  	_, err := env.acquireNode(
   382  		suite.callCtx,
   383  		"", "", "",
   384  		constraints.Value{Spaces: stringslicep("space-1", "^space-2", "space-3", "^space-4")},
   385  		nil, nil,
   386  	)
   387  	c.Check(err, jc.ErrorIsNil)
   388  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   389  	nodeValues, found := requestValues["node0"]
   390  	c.Assert(found, jc.IsTrue)
   391  	c.Check(nodeValues[0].Get("interfaces"), gc.Equals, "0:space=2;1:space=4")
   392  	c.Check(nodeValues[0]["not_networks"], jc.DeepEquals, []string{"space:3", "space:5"})
   393  }
   394  
   395  func (suite *environSuite) createFourSpaces(c *gc.C) {
   396  	server := suite.testMAASObject.TestServer
   397  	server.SetVersionJSON(`{"capabilities": ["network-deployment-ubuntu"]}`)
   398  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-1"}))
   399  	suite.addSubnet(c, 1, 1, "node1")
   400  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-2"}))
   401  	suite.addSubnet(c, 2, 2, "node1")
   402  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-3"}))
   403  	suite.addSubnet(c, 3, 3, "node1")
   404  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-4"}))
   405  	suite.addSubnet(c, 4, 4, "node1")
   406  }
   407  
   408  func (suite *environSuite) TestAcquireNodeDisambiguatesNamedLabelsFromIndexedUpToALimit(c *gc.C) {
   409  	suite.createFourSpaces(c)
   410  	var shortLimit uint = 0
   411  	suite.PatchValue(&numericLabelLimit, shortLimit)
   412  	env := suite.makeEnviron()
   413  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0"}`)
   414  
   415  	_, err := env.acquireNode(
   416  		suite.callCtx,
   417  		"", "", "",
   418  		constraints.Value{Spaces: stringslicep("space-1", "^space-2", "space-3", "^space-4")},
   419  		[]interfaceBinding{{"0", "first-clash"}, {"1", "final-clash"}},
   420  		nil,
   421  	)
   422  	c.Assert(err, gc.ErrorMatches, `too many conflicting numeric labels, giving up.`)
   423  }
   424  
   425  func (suite *environSuite) TestAcquireNodeStorage(c *gc.C) {
   426  	server := suite.testMAASObject.TestServer
   427  	for i, test := range []struct {
   428  		volumes  []volumeInfo
   429  		expected string
   430  	}{{
   431  		volumes:  nil,
   432  		expected: "",
   433  	}, {
   434  		volumes:  []volumeInfo{{"volume-1", 1234, nil}},
   435  		expected: "volume-1:1234",
   436  	}, {
   437  		volumes:  []volumeInfo{{"", 1234, []string{"tag1", "tag2"}}},
   438  		expected: "1234(tag1,tag2)",
   439  	}, {
   440  		volumes:  []volumeInfo{{"volume-1", 1234, []string{"tag1", "tag2"}}},
   441  		expected: "volume-1:1234(tag1,tag2)",
   442  	}, {
   443  		volumes: []volumeInfo{
   444  			{"volume-1", 1234, []string{"tag1", "tag2"}},
   445  			{"volume-2", 4567, []string{"tag1", "tag3"}},
   446  		},
   447  		expected: "volume-1:1234(tag1,tag2),volume-2:4567(tag1,tag3)",
   448  	}} {
   449  		c.Logf("test #%d: volumes=%v", i, test.volumes)
   450  		env := suite.makeEnviron()
   451  		server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-1"}))
   452  		server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   453  		suite.addSubnet(c, 1, 1, "node0")
   454  		_, err := env.acquireNode(suite.callCtx, "", "", "", constraints.Value{}, nil, test.volumes)
   455  		c.Check(err, jc.ErrorIsNil)
   456  		requestValues := server.NodeOperationRequestValues()
   457  		nodeRequestValues, found := requestValues["node0"]
   458  		if c.Check(found, jc.IsTrue) {
   459  			c.Check(nodeRequestValues[0].Get("storage"), gc.Equals, test.expected)
   460  		}
   461  		suite.testMAASObject.TestServer.Clear()
   462  	}
   463  }
   464  
   465  func (suite *environSuite) TestAcquireNodeInterfaces(c *gc.C) {
   466  	server := suite.testMAASObject.TestServer
   467  	// Add some constraints, including spaces to verify specified bindings
   468  	// always override any spaces constraints.
   469  	cons := constraints.Value{
   470  		Spaces: stringslicep("foo", "^bar"),
   471  	}
   472  	// In the tests below "space:5" means foo, "space:6" means bar.
   473  	for i, test := range []struct {
   474  		interfaces        []interfaceBinding
   475  		expectedPositives string
   476  		expectedNegatives string
   477  		expectedError     string
   478  	}{{ // without specified bindings, spaces constraints are used instead.
   479  		interfaces:        nil,
   480  		expectedPositives: "0:space=5",
   481  		expectedNegatives: "space:6",
   482  		expectedError:     "",
   483  	}, {
   484  		interfaces:        []interfaceBinding{{"name-1", "space-1"}},
   485  		expectedPositives: "name-1:space=space-1;0:space=5",
   486  		expectedNegatives: "space:6",
   487  	}, {
   488  		interfaces: []interfaceBinding{
   489  			{"name-1", "1"},
   490  			{"name-2", "2"},
   491  			{"name-3", "3"},
   492  		},
   493  		expectedPositives: "name-1:space=1;name-2:space=2;name-3:space=3;0:space=5",
   494  		expectedNegatives: "space:6",
   495  	}, {
   496  		interfaces:        []interfaceBinding{{"", "anything"}},
   497  		expectedPositives: "0:space=anything;1:space=5",
   498  		expectedNegatives: "space:6",
   499  	}, {
   500  		interfaces:    []interfaceBinding{{"shared-db", "6"}},
   501  		expectedError: `negative space "bar" from constraints clashes with interface bindings`,
   502  	}, {
   503  		interfaces: []interfaceBinding{
   504  			{"shared-db", "1"},
   505  			{"db", "1"},
   506  		},
   507  		expectedPositives: "shared-db:space=1;db:space=1;0:space=5",
   508  		expectedNegatives: "space:6",
   509  	}, {
   510  		interfaces:    []interfaceBinding{{"", ""}},
   511  		expectedError: `invalid interface binding "": space provider ID is required`,
   512  	}, {
   513  		interfaces: []interfaceBinding{
   514  			{"valid", "ok"},
   515  			{"", "valid-but-ignored-space"},
   516  			{"valid-name-empty-space", ""},
   517  			{"", ""},
   518  		},
   519  		expectedError: `invalid interface binding "valid-name-empty-space": space provider ID is required`,
   520  	}, {
   521  		interfaces:    []interfaceBinding{{"foo", ""}},
   522  		expectedError: `invalid interface binding "foo": space provider ID is required`,
   523  	}, {
   524  		interfaces: []interfaceBinding{
   525  			{"bar", ""},
   526  			{"valid", "ok"},
   527  			{"", "valid-but-ignored-space"},
   528  			{"", ""},
   529  		},
   530  		expectedError: `invalid interface binding "bar": space provider ID is required`,
   531  	}, {
   532  		interfaces: []interfaceBinding{
   533  			{"dup-name", "1"},
   534  			{"dup-name", "2"},
   535  		},
   536  		expectedError: `duplicated interface binding "dup-name"`,
   537  	}, {
   538  		interfaces: []interfaceBinding{
   539  			{"valid-1", "0"},
   540  			{"dup-name", "1"},
   541  			{"dup-name", "2"},
   542  			{"valid-2", "3"},
   543  		},
   544  		expectedError: `duplicated interface binding "dup-name"`,
   545  	}} {
   546  		suite.testMAASObject.TestServer.Clear()
   547  		c.Logf("test #%d: interfaces=%v", i, test.interfaces)
   548  		suite.createFourSpaces(c)
   549  		server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "foo"}))
   550  		suite.addSubnetWithSpace(c, 6, 6, "foo", "node1")
   551  		server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "bar"}))
   552  		suite.addSubnetWithSpace(c, 7, 7, "bar", "node1")
   553  		env := suite.makeEnviron()
   554  		server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   555  		_, err := env.acquireNode(suite.callCtx, "", "", "", cons, test.interfaces, nil)
   556  		if test.expectedError != "" {
   557  			c.Check(err, gc.ErrorMatches, test.expectedError)
   558  			c.Check(err, jc.Satisfies, errors.IsNotValid)
   559  			continue
   560  		}
   561  		c.Check(err, jc.ErrorIsNil)
   562  		requestValues := server.NodeOperationRequestValues()
   563  		nodeRequestValues, found := requestValues["node0"]
   564  		if c.Check(found, jc.IsTrue) {
   565  
   566  			c.Check(nodeRequestValues[0].Get("interfaces"), gc.Equals, test.expectedPositives)
   567  			c.Check(nodeRequestValues[0].Get("not_networks"), gc.Equals, test.expectedNegatives)
   568  		}
   569  	}
   570  }
   571  
   572  func (suite *environSuite) createFooBarSpaces(c *gc.C) {
   573  	server := suite.testMAASObject.TestServer
   574  	server.SetVersionJSON(`{"capabilities": ["network-deployment-ubuntu"]}`)
   575  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "foo"}))
   576  	suite.addSubnetWithSpace(c, 1, 2, "foo", "node1")
   577  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "bar"}))
   578  	suite.addSubnetWithSpace(c, 2, 3, "bar", "node1")
   579  }
   580  
   581  func (suite *environSuite) TestAcquireNodeConvertsSpaceNames(c *gc.C) {
   582  	server := suite.testMAASObject.TestServer
   583  	suite.createFooBarSpaces(c)
   584  	cons := constraints.Value{
   585  		Spaces: stringslicep("foo", "^bar"),
   586  	}
   587  	env := suite.makeEnviron()
   588  	server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   589  	_, err := env.acquireNode(suite.callCtx, "", "", "", cons, nil, nil)
   590  	c.Assert(err, jc.ErrorIsNil)
   591  	requestValues := server.NodeOperationRequestValues()
   592  	nodeRequestValues, found := requestValues["node0"]
   593  	c.Assert(found, jc.IsTrue)
   594  	c.Check(nodeRequestValues[0].Get("interfaces"), gc.Equals, "0:space=2")
   595  	c.Check(nodeRequestValues[0].Get("not_networks"), gc.Equals, "space:3")
   596  }
   597  
   598  func (suite *environSuite) TestAcquireNodeTranslatesSpaceNames(c *gc.C) {
   599  	server := suite.testMAASObject.TestServer
   600  	suite.createFooBarSpaces(c)
   601  	cons := constraints.Value{
   602  		Spaces: stringslicep("foo-1", "^bar-3"),
   603  	}
   604  	env := suite.makeEnviron()
   605  	server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   606  	_, err := env.acquireNode(suite.callCtx, "", "", "", cons, nil, nil)
   607  	c.Assert(err, jc.ErrorIsNil)
   608  	requestValues := server.NodeOperationRequestValues()
   609  	nodeRequestValues, found := requestValues["node0"]
   610  	c.Assert(found, jc.IsTrue)
   611  	c.Check(nodeRequestValues[0].Get("interfaces"), gc.Equals, "0:space=2")
   612  	c.Check(nodeRequestValues[0].Get("not_networks"), gc.Equals, "space:3")
   613  }
   614  
   615  func (suite *environSuite) TestAcquireNodeUnrecognisedSpace(c *gc.C) {
   616  	server := suite.testMAASObject.TestServer
   617  	suite.createFooBarSpaces(c)
   618  	cons := constraints.Value{
   619  		Spaces: stringslicep("baz"),
   620  	}
   621  	env := suite.makeEnviron()
   622  	server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   623  	_, err := env.acquireNode(suite.callCtx, "", "", "", cons, nil, nil)
   624  	c.Assert(err, gc.ErrorMatches, `unrecognised space in constraint "baz"`)
   625  }