github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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/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  		AvailabilityZones: []string{"foo", "bar"},
   210  		Constraints:       constraints.Value{},
   211  	}
   212  
   213  	node, err := env.selectNode(snArgs)
   214  	c.Assert(err, jc.ErrorIsNil)
   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  		AvailabilityZones: []string{"foo", "bar"},
   223  		Constraints:       constraints.Value{},
   224  	}
   225  
   226  	_, err := env.selectNode(snArgs)
   227  	c.Assert(err, gc.NotNil)
   228  	c.Assert(err, gc.ErrorMatches, `cannot run instances: ServerError: 409 Conflict \(\)`)
   229  }
   230  
   231  func (suite *environSuite) TestAcquireNode(c *gc.C) {
   232  	env := suite.makeEnviron()
   233  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   234  
   235  	_, err := env.acquireNode("", "", constraints.Value{}, nil, nil)
   236  
   237  	c.Check(err, jc.ErrorIsNil)
   238  	operations := suite.testMAASObject.TestServer.NodeOperations()
   239  	actions, found := operations["node0"]
   240  	c.Assert(found, jc.IsTrue)
   241  	c.Check(actions, gc.DeepEquals, []string{"acquire"})
   242  
   243  	// no "name" parameter should have been passed through
   244  	values := suite.testMAASObject.TestServer.NodeOperationRequestValues()["node0"][0]
   245  	_, found = values["name"]
   246  	c.Assert(found, jc.IsFalse)
   247  }
   248  
   249  func (suite *environSuite) TestAcquireNodeByName(c *gc.C) {
   250  	env := suite.makeEnviron()
   251  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   252  
   253  	_, err := env.acquireNode("host0", "", constraints.Value{}, nil, nil)
   254  
   255  	c.Check(err, jc.ErrorIsNil)
   256  	operations := suite.testMAASObject.TestServer.NodeOperations()
   257  	actions, found := operations["node0"]
   258  	c.Assert(found, jc.IsTrue)
   259  	c.Check(actions, gc.DeepEquals, []string{"acquire"})
   260  
   261  	// no "name" parameter should have been passed through
   262  	values := suite.testMAASObject.TestServer.NodeOperationRequestValues()["node0"][0]
   263  	nodeName := values.Get("name")
   264  	c.Assert(nodeName, gc.Equals, "host0")
   265  }
   266  
   267  func (suite *environSuite) TestAcquireNodeTakesConstraintsIntoAccount(c *gc.C) {
   268  	env := suite.makeEnviron()
   269  	suite.testMAASObject.TestServer.NewNode(
   270  		`{"system_id": "node0", "hostname": "host0", "architecture": "arm/generic", "memory": 2048}`,
   271  	)
   272  	constraints := constraints.Value{Arch: stringp("arm"), Mem: uint64p(1024)}
   273  
   274  	_, err := env.acquireNode("", "", constraints, nil, nil)
   275  
   276  	c.Check(err, jc.ErrorIsNil)
   277  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   278  	nodeRequestValues, found := requestValues["node0"]
   279  	c.Assert(found, jc.IsTrue)
   280  	c.Assert(nodeRequestValues[0].Get("arch"), gc.Equals, "arm")
   281  	c.Assert(nodeRequestValues[0].Get("mem"), gc.Equals, "1024")
   282  }
   283  
   284  func (suite *environSuite) TestParseDelimitedValues(c *gc.C) {
   285  	for i, test := range []struct {
   286  		about     string
   287  		input     []string
   288  		positives []string
   289  		negatives []string
   290  	}{{
   291  		about:     "nil input",
   292  		input:     nil,
   293  		positives: []string{},
   294  		negatives: []string{},
   295  	}, {
   296  		about:     "empty input",
   297  		input:     []string{},
   298  		positives: []string{},
   299  		negatives: []string{},
   300  	}, {
   301  		about:     "values list with embedded whitespace",
   302  		input:     []string{"   val1  ", " val2", " ^ not Val 3  ", "  ", " ", "^", "", "^ notVal4   "},
   303  		positives: []string{"   val1  ", " val2", " ^ not Val 3  ", "  ", " "},
   304  		negatives: []string{" notVal4   "},
   305  	}, {
   306  		about:     "only positives",
   307  		input:     []string{"val1", "val2", "val3"},
   308  		positives: []string{"val1", "val2", "val3"},
   309  		negatives: []string{},
   310  	}, {
   311  		about:     "only negatives",
   312  		input:     []string{"^val1", "^val2", "^val3"},
   313  		positives: []string{},
   314  		negatives: []string{"val1", "val2", "val3"},
   315  	}, {
   316  		about:     "multi-caret negatives",
   317  		input:     []string{"^foo^", "^v^a^l2", "  ^^ ^", "^v^al3", "^^", "^"},
   318  		positives: []string{"  ^^ ^"},
   319  		negatives: []string{"foo^", "v^a^l2", "v^al3", "^"},
   320  	}, {
   321  		about:     "both positives and negatives",
   322  		input:     []string{"^val1", "val2", "^val3", "val4"},
   323  		positives: []string{"val2", "val4"},
   324  		negatives: []string{"val1", "val3"},
   325  	}, {
   326  		about:     "single positive value",
   327  		input:     []string{"val1"},
   328  		positives: []string{"val1"},
   329  		negatives: []string{},
   330  	}, {
   331  		about:     "single negative value",
   332  		input:     []string{"^val1"},
   333  		positives: []string{},
   334  		negatives: []string{"val1"},
   335  	}} {
   336  		c.Logf("test %d: %s", i, test.about)
   337  		positives, negatives := parseDelimitedValues(test.input)
   338  		c.Check(positives, jc.DeepEquals, test.positives)
   339  		c.Check(negatives, jc.DeepEquals, test.negatives)
   340  	}
   341  }
   342  
   343  func (suite *environSuite) TestAcquireNodePassedAgentName(c *gc.C) {
   344  	env := suite.makeEnviron()
   345  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   346  
   347  	_, err := env.acquireNode("", "", constraints.Value{}, nil, nil)
   348  
   349  	c.Check(err, jc.ErrorIsNil)
   350  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   351  	nodeRequestValues, found := requestValues["node0"]
   352  	c.Assert(found, jc.IsTrue)
   353  	c.Assert(nodeRequestValues[0].Get("agent_name"), gc.Equals, exampleAgentName)
   354  }
   355  
   356  func (suite *environSuite) TestAcquireNodePassesPositiveAndNegativeTags(c *gc.C) {
   357  	env := suite.makeEnviron()
   358  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0"}`)
   359  
   360  	_, err := env.acquireNode(
   361  		"", "",
   362  		constraints.Value{Tags: stringslicep("tag1", "^tag2", "tag3", "^tag4")},
   363  		nil, nil,
   364  	)
   365  
   366  	c.Check(err, jc.ErrorIsNil)
   367  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   368  	nodeValues, found := requestValues["node0"]
   369  	c.Assert(found, jc.IsTrue)
   370  	c.Assert(nodeValues[0].Get("tags"), gc.Equals, "tag1,tag3")
   371  	c.Assert(nodeValues[0].Get("not_tags"), gc.Equals, "tag2,tag4")
   372  }
   373  
   374  func (suite *environSuite) TestAcquireNodePassesPositiveAndNegativeSpaces(c *gc.C) {
   375  	suite.createFourSpaces(c)
   376  	env := suite.makeEnviron()
   377  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0"}`)
   378  
   379  	_, err := env.acquireNode(
   380  		"", "",
   381  		constraints.Value{Spaces: stringslicep("space-1", "^space-2", "space-3", "^space-4")},
   382  		nil, nil,
   383  	)
   384  	c.Check(err, jc.ErrorIsNil)
   385  	requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues()
   386  	nodeValues, found := requestValues["node0"]
   387  	c.Assert(found, jc.IsTrue)
   388  	c.Check(nodeValues[0].Get("interfaces"), gc.Equals, "0:space=2;1:space=4")
   389  	c.Check(nodeValues[0].Get("not_networks"), gc.Equals, "space:3,space:5")
   390  }
   391  
   392  func (suite *environSuite) createFourSpaces(c *gc.C) {
   393  	server := suite.testMAASObject.TestServer
   394  	server.SetVersionJSON(`{"capabilities": ["network-deployment-ubuntu"]}`)
   395  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-1"}))
   396  	suite.addSubnet(c, 1, 1, "node1")
   397  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-2"}))
   398  	suite.addSubnet(c, 2, 2, "node1")
   399  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-3"}))
   400  	suite.addSubnet(c, 3, 3, "node1")
   401  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-4"}))
   402  	suite.addSubnet(c, 4, 4, "node1")
   403  }
   404  
   405  func (suite *environSuite) TestAcquireNodeDisambiguatesNamedLabelsFromIndexedUpToALimit(c *gc.C) {
   406  	suite.createFourSpaces(c)
   407  	var shortLimit uint = 0
   408  	suite.PatchValue(&numericLabelLimit, shortLimit)
   409  	env := suite.makeEnviron()
   410  	suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0"}`)
   411  
   412  	_, err := env.acquireNode(
   413  		"", "",
   414  		constraints.Value{Spaces: stringslicep("space-1", "^space-2", "space-3", "^space-4")},
   415  		[]interfaceBinding{{"0", "first-clash"}, {"1", "final-clash"}},
   416  		nil,
   417  	)
   418  	c.Assert(err, gc.ErrorMatches, `too many conflicting numeric labels, giving up.`)
   419  }
   420  
   421  func (suite *environSuite) TestAcquireNodeStorage(c *gc.C) {
   422  	server := suite.testMAASObject.TestServer
   423  	for i, test := range []struct {
   424  		volumes  []volumeInfo
   425  		expected string
   426  	}{{
   427  		volumes:  nil,
   428  		expected: "",
   429  	}, {
   430  		volumes:  []volumeInfo{{"volume-1", 1234, nil}},
   431  		expected: "volume-1:1234",
   432  	}, {
   433  		volumes:  []volumeInfo{{"", 1234, []string{"tag1", "tag2"}}},
   434  		expected: "1234(tag1,tag2)",
   435  	}, {
   436  		volumes:  []volumeInfo{{"volume-1", 1234, []string{"tag1", "tag2"}}},
   437  		expected: "volume-1:1234(tag1,tag2)",
   438  	}, {
   439  		volumes: []volumeInfo{
   440  			{"volume-1", 1234, []string{"tag1", "tag2"}},
   441  			{"volume-2", 4567, []string{"tag1", "tag3"}},
   442  		},
   443  		expected: "volume-1:1234(tag1,tag2),volume-2:4567(tag1,tag3)",
   444  	}} {
   445  		c.Logf("test #%d: volumes=%v", i, test.volumes)
   446  		env := suite.makeEnviron()
   447  		server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "space-1"}))
   448  		server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   449  		suite.addSubnet(c, 1, 1, "node0")
   450  		_, err := env.acquireNode("", "", constraints.Value{}, nil, test.volumes)
   451  		c.Check(err, jc.ErrorIsNil)
   452  		requestValues := server.NodeOperationRequestValues()
   453  		nodeRequestValues, found := requestValues["node0"]
   454  		if c.Check(found, jc.IsTrue) {
   455  			c.Check(nodeRequestValues[0].Get("storage"), gc.Equals, test.expected)
   456  		}
   457  		suite.testMAASObject.TestServer.Clear()
   458  	}
   459  }
   460  
   461  func (suite *environSuite) TestAcquireNodeInterfaces(c *gc.C) {
   462  	server := suite.testMAASObject.TestServer
   463  	// Add some constraints, including spaces to verify specified bindings
   464  	// always override any spaces constraints.
   465  	cons := constraints.Value{
   466  		Spaces: stringslicep("foo", "^bar"),
   467  	}
   468  	// In the tests below "space:5" means foo, "space:6" means bar.
   469  	for i, test := range []struct {
   470  		interfaces        []interfaceBinding
   471  		expectedPositives string
   472  		expectedNegatives string
   473  		expectedError     string
   474  	}{{ // without specified bindings, spaces constraints are used instead.
   475  		interfaces:        nil,
   476  		expectedPositives: "0:space=5",
   477  		expectedNegatives: "space:6",
   478  		expectedError:     "",
   479  	}, {
   480  		interfaces:        []interfaceBinding{{"name-1", "space-1"}},
   481  		expectedPositives: "name-1:space=space-1;0:space=5",
   482  		expectedNegatives: "space:6",
   483  	}, {
   484  		interfaces: []interfaceBinding{
   485  			{"name-1", "1"},
   486  			{"name-2", "2"},
   487  			{"name-3", "3"},
   488  		},
   489  		expectedPositives: "name-1:space=1;name-2:space=2;name-3:space=3;0:space=5",
   490  		expectedNegatives: "space:6",
   491  	}, {
   492  		interfaces:    []interfaceBinding{{"", "anything"}},
   493  		expectedError: "interface bindings cannot have empty names",
   494  	}, {
   495  		interfaces:    []interfaceBinding{{"shared-db", "6"}},
   496  		expectedError: `negative space "bar" from constraints clashes with interface bindings`,
   497  	}, {
   498  		interfaces: []interfaceBinding{
   499  			{"shared-db", "1"},
   500  			{"db", "1"},
   501  		},
   502  		expectedPositives: "shared-db:space=1;db:space=1;0:space=5",
   503  		expectedNegatives: "space:6",
   504  	}, {
   505  		interfaces:    []interfaceBinding{{"", ""}},
   506  		expectedError: "interface bindings cannot have empty names",
   507  	}, {
   508  		interfaces: []interfaceBinding{
   509  			{"valid", "ok"},
   510  			{"", "valid-but-ignored-space"},
   511  			{"valid-name-empty-space", ""},
   512  			{"", ""},
   513  		},
   514  		expectedError: "interface bindings cannot have empty names",
   515  	}, {
   516  		interfaces:    []interfaceBinding{{"foo", ""}},
   517  		expectedError: `invalid interface binding "foo": space provider ID is required`,
   518  	}, {
   519  		interfaces: []interfaceBinding{
   520  			{"bar", ""},
   521  			{"valid", "ok"},
   522  			{"", "valid-but-ignored-space"},
   523  			{"", ""},
   524  		},
   525  		expectedError: `invalid interface binding "bar": space provider ID is required`,
   526  	}, {
   527  		interfaces: []interfaceBinding{
   528  			{"dup-name", "1"},
   529  			{"dup-name", "2"},
   530  		},
   531  		expectedError: `duplicated interface binding "dup-name"`,
   532  	}, {
   533  		interfaces: []interfaceBinding{
   534  			{"valid-1", "0"},
   535  			{"dup-name", "1"},
   536  			{"dup-name", "2"},
   537  			{"valid-2", "3"},
   538  		},
   539  		expectedError: `duplicated interface binding "dup-name"`,
   540  	}} {
   541  		suite.testMAASObject.TestServer.Clear()
   542  		c.Logf("test #%d: interfaces=%v", i, test.interfaces)
   543  		suite.createFourSpaces(c)
   544  		server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "foo"}))
   545  		suite.addSubnetWithSpace(c, 6, 6, "foo", "node1")
   546  		server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "bar"}))
   547  		suite.addSubnetWithSpace(c, 7, 7, "bar", "node1")
   548  		env := suite.makeEnviron()
   549  		server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   550  		_, err := env.acquireNode("", "", cons, test.interfaces, nil)
   551  		if test.expectedError != "" {
   552  			c.Check(err, gc.ErrorMatches, test.expectedError)
   553  			c.Check(err, jc.Satisfies, errors.IsNotValid)
   554  			continue
   555  		}
   556  		c.Check(err, jc.ErrorIsNil)
   557  		requestValues := server.NodeOperationRequestValues()
   558  		nodeRequestValues, found := requestValues["node0"]
   559  		if c.Check(found, jc.IsTrue) {
   560  
   561  			c.Check(nodeRequestValues[0].Get("interfaces"), gc.Equals, test.expectedPositives)
   562  			c.Check(nodeRequestValues[0].Get("not_networks"), gc.Equals, test.expectedNegatives)
   563  		}
   564  	}
   565  }
   566  
   567  func (suite *environSuite) createFooBarSpaces(c *gc.C) {
   568  	server := suite.testMAASObject.TestServer
   569  	server.SetVersionJSON(`{"capabilities": ["network-deployment-ubuntu"]}`)
   570  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "foo"}))
   571  	suite.addSubnetWithSpace(c, 1, 2, "foo", "node1")
   572  	server.NewSpace(spaceJSON(gomaasapi.CreateSpace{Name: "bar"}))
   573  	suite.addSubnetWithSpace(c, 2, 3, "bar", "node1")
   574  }
   575  
   576  func (suite *environSuite) TestAcquireNodeConvertsSpaceNames(c *gc.C) {
   577  	server := suite.testMAASObject.TestServer
   578  	suite.createFooBarSpaces(c)
   579  	cons := constraints.Value{
   580  		Spaces: stringslicep("foo", "^bar"),
   581  	}
   582  	env := suite.makeEnviron()
   583  	server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   584  	_, err := env.acquireNode("", "", cons, nil, nil)
   585  	c.Assert(err, jc.ErrorIsNil)
   586  	requestValues := server.NodeOperationRequestValues()
   587  	nodeRequestValues, found := requestValues["node0"]
   588  	c.Assert(found, jc.IsTrue)
   589  	c.Check(nodeRequestValues[0].Get("interfaces"), gc.Equals, "0:space=2")
   590  	c.Check(nodeRequestValues[0].Get("not_networks"), gc.Equals, "space:3")
   591  }
   592  
   593  func (suite *environSuite) TestAcquireNodeTranslatesSpaceNames(c *gc.C) {
   594  	server := suite.testMAASObject.TestServer
   595  	suite.createFooBarSpaces(c)
   596  	cons := constraints.Value{
   597  		Spaces: stringslicep("foo-1", "^bar-3"),
   598  	}
   599  	env := suite.makeEnviron()
   600  	server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   601  	_, err := env.acquireNode("", "", cons, nil, nil)
   602  	c.Assert(err, jc.ErrorIsNil)
   603  	requestValues := server.NodeOperationRequestValues()
   604  	nodeRequestValues, found := requestValues["node0"]
   605  	c.Assert(found, jc.IsTrue)
   606  	c.Check(nodeRequestValues[0].Get("interfaces"), gc.Equals, "0:space=2")
   607  	c.Check(nodeRequestValues[0].Get("not_networks"), gc.Equals, "space:3")
   608  }
   609  
   610  func (suite *environSuite) TestAcquireNodeUnrecognisedSpace(c *gc.C) {
   611  	server := suite.testMAASObject.TestServer
   612  	suite.createFooBarSpaces(c)
   613  	cons := constraints.Value{
   614  		Spaces: stringslicep("baz"),
   615  	}
   616  	env := suite.makeEnviron()
   617  	server.NewNode(`{"system_id": "node0", "hostname": "host0"}`)
   618  	_, err := env.acquireNode("", "", cons, nil, nil)
   619  	c.Assert(err, gc.ErrorMatches, `unrecognised space in constraint "baz"`)
   620  }