github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/cloud/add_test.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package cloud_test
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"os"
    12  	"path"
    13  	"regexp"
    14  	"strings"
    15  
    16  	"github.com/juju/cmd"
    17  	"github.com/juju/cmd/cmdtesting"
    18  	"github.com/juju/errors"
    19  	"github.com/juju/loggo"
    20  	jujutesting "github.com/juju/testing"
    21  	jc "github.com/juju/testing/checkers"
    22  	gc "gopkg.in/check.v1"
    23  	"gopkg.in/yaml.v2"
    24  
    25  	jujucloud "github.com/juju/juju/cloud"
    26  	"github.com/juju/juju/cmd/juju/cloud"
    27  	"github.com/juju/juju/environs"
    28  	"github.com/juju/juju/jujuclient"
    29  	"github.com/juju/juju/testing"
    30  )
    31  
    32  type addSuite struct {
    33  	jujutesting.IsolationSuite
    34  }
    35  
    36  var _ = gc.Suite(&addSuite{})
    37  
    38  func newFakeCloudMetadataStore() *fakeCloudMetadataStore {
    39  	var logger loggo.Logger
    40  	return &fakeCloudMetadataStore{CallMocker: jujutesting.NewCallMocker(logger)}
    41  }
    42  
    43  type fakeCloudMetadataStore struct {
    44  	*jujutesting.CallMocker
    45  }
    46  
    47  func (f *fakeCloudMetadataStore) ParseCloudMetadataFile(path string) (map[string]jujucloud.Cloud, error) {
    48  	results := f.MethodCall(f, "ParseCloudMetadataFile", path)
    49  	return results[0].(map[string]jujucloud.Cloud), jujutesting.TypeAssertError(results[1])
    50  }
    51  
    52  func (f *fakeCloudMetadataStore) PublicCloudMetadata(searchPaths ...string) (result map[string]jujucloud.Cloud, fallbackUsed bool, _ error) {
    53  	results := f.MethodCall(f, "PublicCloudMetadata", searchPaths)
    54  	return results[0].(map[string]jujucloud.Cloud), results[1].(bool), jujutesting.TypeAssertError(results[2])
    55  }
    56  
    57  func (f *fakeCloudMetadataStore) PersonalCloudMetadata() (map[string]jujucloud.Cloud, error) {
    58  	results := f.MethodCall(f, "PersonalCloudMetadata")
    59  	return results[0].(map[string]jujucloud.Cloud), jujutesting.TypeAssertError(results[1])
    60  }
    61  
    62  func (f *fakeCloudMetadataStore) WritePersonalCloudMetadata(cloudsMap map[string]jujucloud.Cloud) error {
    63  	results := f.MethodCall(f, "WritePersonalCloudMetadata", cloudsMap)
    64  	return jujutesting.TypeAssertError(results[0])
    65  }
    66  
    67  func (f *fakeCloudMetadataStore) ParseOneCloud(data []byte) (jujucloud.Cloud, error) {
    68  	results := f.MethodCall(f, "ParseOneCloud", data)
    69  	if len(results) != 2 {
    70  		fmt.Printf("ParseOneCloud()\n(%s)\n", string(data))
    71  		return jujucloud.Cloud{}, errors.New("ParseOneCloud failed, not enough results")
    72  	}
    73  	return results[0].(jujucloud.Cloud), jujutesting.TypeAssertError(results[1])
    74  }
    75  
    76  func (s *addSuite) TestAddBadArgs(c *gc.C) {
    77  	_, err := cmdtesting.RunCommand(c, cloud.NewAddCloudCommand(nil), "cloud", "cloud.yaml", "extra")
    78  	c.Assert(err, gc.ErrorMatches, `unrecognized args: \["extra"\]`)
    79  }
    80  
    81  var (
    82  	homeStackYamlFile = `
    83          clouds:
    84            homestack:
    85              type: openstack
    86              auth-types: [access-key]
    87              endpoint: "http://homestack"
    88              regions:
    89                london:
    90                  endpoint: "http://london/1.0"`
    91  
    92  	homestackCloud = jujucloud.Cloud{
    93  		Name:      "homestack",
    94  		Type:      "openstack",
    95  		AuthTypes: []jujucloud.AuthType{"userpass", "access-key"},
    96  		Endpoint:  "http://homestack",
    97  		Regions: []jujucloud.Region{
    98  			{
    99  				Name:     "london",
   100  				Endpoint: "http://london/1.0",
   101  			},
   102  		},
   103  	}
   104  
   105  	localhostYamlFile = `
   106          clouds:
   107            localhost:
   108              type: lxd`
   109  
   110  	awsYamlFile = `
   111          clouds:
   112            aws:
   113              type: ec2
   114              auth-types: [access-key]
   115              regions:
   116                us-east-1:
   117                  endpoint: "https://us-east-1.aws.amazon.com/v1.2/"`
   118  
   119  	garageMaasYamlFile = `
   120          clouds:
   121            garage-maas:
   122              type: maas
   123              auth-types: [oauth1]
   124              endpoint: "http://garagemaas"`
   125  
   126  	garageMAASCloud = jujucloud.Cloud{
   127  		Name:      "garage-maas",
   128  		Type:      "maas",
   129  		AuthTypes: []jujucloud.AuthType{"oauth1"},
   130  		Endpoint:  "http://garagemaas",
   131  	}
   132  
   133  	manualCloud = jujucloud.Cloud{
   134  		Name:      "manual",
   135  		Type:      "manual",
   136  		AuthTypes: []jujucloud.AuthType{"manual"},
   137  		Endpoint:  "192.168.1.6",
   138  	}
   139  )
   140  
   141  func homestackMetadata() map[string]jujucloud.Cloud {
   142  	return map[string]jujucloud.Cloud{"homestack": homestackCloud}
   143  }
   144  
   145  func (*addSuite) TestAddBadFilename(c *gc.C) {
   146  	fake := newFakeCloudMetadataStore()
   147  	badFileErr := errors.New("")
   148  	fake.Call("ParseCloudMetadataFile", "somefile.yaml").Returns(map[string]jujucloud.Cloud{}, badFileErr)
   149  
   150  	addCmd := cloud.NewAddCloudCommand(fake)
   151  	_, err := cmdtesting.RunCommand(c, addCmd, "cloud", "somefile.yaml")
   152  	c.Check(errors.Cause(err), gc.Equals, badFileErr)
   153  }
   154  
   155  func (*addSuite) TestAddBadCloudName(c *gc.C) {
   156  	fake := newFakeCloudMetadataStore()
   157  	fake.Call("ParseCloudMetadataFile", "testFile").Returns(map[string]jujucloud.Cloud{}, nil)
   158  
   159  	addCmd := cloud.NewAddCloudCommand(fake)
   160  	_, err := cmdtesting.RunCommand(c, addCmd, "cloud", "testFile")
   161  	c.Assert(err, gc.ErrorMatches, `cloud "cloud" not found in file .*`)
   162  }
   163  
   164  func (*addSuite) TestAddInvalidCloudName(c *gc.C) {
   165  	fake := newFakeCloudMetadataStore()
   166  	fake.Call("ParseCloudMetadataFile", "testFile").Returns(map[string]jujucloud.Cloud{}, nil)
   167  
   168  	addCmd := cloud.NewAddCloudCommand(fake)
   169  	_, err := cmdtesting.RunCommand(c, addCmd, "bad^cloud", "testFile")
   170  	c.Assert(err, gc.ErrorMatches, `cloud name "bad\^cloud" not valid`)
   171  }
   172  
   173  func (*addSuite) TestAddExisting(c *gc.C) {
   174  	fake := newFakeCloudMetadataStore()
   175  
   176  	cloudFile := prepareTestCloudYaml(c, homeStackYamlFile)
   177  	defer cloudFile.Close()
   178  	defer os.Remove(cloudFile.Name())
   179  
   180  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudFile.Name())
   181  	c.Assert(err, jc.ErrorIsNil)
   182  
   183  	fake.Call("ParseCloudMetadataFile", cloudFile.Name()).Returns(mockCloud, nil)
   184  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   185  	fake.Call("PersonalCloudMetadata").Returns(mockCloud, nil)
   186  
   187  	_, err = cmdtesting.RunCommand(c, cloud.NewAddCloudCommand(fake), "homestack", cloudFile.Name())
   188  	c.Assert(err, gc.ErrorMatches, `"homestack" already exists; use --replace to replace this existing cloud`)
   189  }
   190  
   191  func (*addSuite) TestAddExistingReplace(c *gc.C) {
   192  	fake := newFakeCloudMetadataStore()
   193  
   194  	cloudFile := prepareTestCloudYaml(c, homeStackYamlFile)
   195  	defer cloudFile.Close()
   196  	defer os.Remove(cloudFile.Name())
   197  
   198  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudFile.Name())
   199  	c.Assert(err, jc.ErrorIsNil)
   200  
   201  	fake.Call("ParseCloudMetadataFile", cloudFile.Name()).Returns(mockCloud, nil)
   202  	fake.Call("PersonalCloudMetadata").Returns(mockCloud, nil)
   203  	numCallsToWrite := fake.Call("WritePersonalCloudMetadata", mockCloud).Returns(nil)
   204  
   205  	_, err = cmdtesting.RunCommand(c, cloud.NewAddCloudCommand(fake), "homestack", cloudFile.Name(), "--replace")
   206  	c.Assert(err, jc.ErrorIsNil)
   207  
   208  	c.Check(numCallsToWrite(), gc.Equals, 1)
   209  }
   210  
   211  func (*addSuite) TestAddExistingPublic(c *gc.C) {
   212  	cloudFile := prepareTestCloudYaml(c, awsYamlFile)
   213  	defer cloudFile.Close()
   214  	defer os.Remove(cloudFile.Name())
   215  
   216  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudFile.Name())
   217  	c.Assert(err, jc.ErrorIsNil)
   218  
   219  	fake := newFakeCloudMetadataStore()
   220  	fake.Call("ParseCloudMetadataFile", cloudFile.Name()).Returns(mockCloud, nil)
   221  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(mockCloud, false, nil)
   222  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   223  
   224  	_, err = cmdtesting.RunCommand(c, cloud.NewAddCloudCommand(fake), "aws", cloudFile.Name())
   225  	c.Assert(err, gc.ErrorMatches, `"aws" is the name of a public cloud; use --replace to override this definition`)
   226  }
   227  
   228  func (*addSuite) TestAddExistingBuiltin(c *gc.C) {
   229  	cloudFile := prepareTestCloudYaml(c, localhostYamlFile)
   230  	defer cloudFile.Close()
   231  	defer os.Remove(cloudFile.Name())
   232  
   233  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudFile.Name())
   234  	c.Assert(err, jc.ErrorIsNil)
   235  
   236  	fake := newFakeCloudMetadataStore()
   237  	fake.Call("ParseCloudMetadataFile", cloudFile.Name()).Returns(mockCloud, nil)
   238  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   239  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   240  
   241  	_, err = cmdtesting.RunCommand(c, cloud.NewAddCloudCommand(fake), "localhost", cloudFile.Name())
   242  	c.Assert(err, gc.ErrorMatches, `"localhost" is the name of a built-in cloud; use --replace to override this definition`)
   243  }
   244  
   245  func (*addSuite) TestAddExistingPublicReplace(c *gc.C) {
   246  	cloudFile := prepareTestCloudYaml(c, awsYamlFile)
   247  	defer cloudFile.Close()
   248  	defer os.Remove(cloudFile.Name())
   249  
   250  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudFile.Name())
   251  	c.Assert(err, jc.ErrorIsNil)
   252  
   253  	fake := newFakeCloudMetadataStore()
   254  	fake.Call("ParseCloudMetadataFile", cloudFile.Name()).Returns(mockCloud, nil)
   255  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(mockCloud, false, nil)
   256  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   257  	writeCall := fake.Call("WritePersonalCloudMetadata", mockCloud).Returns(nil)
   258  
   259  	_, err = cmdtesting.RunCommand(c, cloud.NewAddCloudCommand(fake), "aws", cloudFile.Name(), "--replace")
   260  	c.Assert(err, jc.ErrorIsNil)
   261  
   262  	c.Check(writeCall(), gc.Equals, 1)
   263  }
   264  
   265  func (*addSuite) TestAddNew(c *gc.C) {
   266  	cloudFile := prepareTestCloudYaml(c, garageMaasYamlFile)
   267  	defer cloudFile.Close()
   268  	defer os.Remove(cloudFile.Name())
   269  
   270  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudFile.Name())
   271  	c.Assert(err, jc.ErrorIsNil)
   272  
   273  	fake := newFakeCloudMetadataStore()
   274  	fake.Call("ParseCloudMetadataFile", cloudFile.Name()).Returns(mockCloud, nil)
   275  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   276  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   277  	numCallsToWrite := fake.Call("WritePersonalCloudMetadata", mockCloud).Returns(nil)
   278  
   279  	_, err = cmdtesting.RunCommand(c, cloud.NewAddCloudCommand(fake), "garage-maas", cloudFile.Name())
   280  	c.Assert(err, jc.ErrorIsNil)
   281  	c.Check(numCallsToWrite(), gc.Equals, 1)
   282  }
   283  
   284  func (*addSuite) TestAddNewInvalidAuthType(c *gc.C) {
   285  	fake := newFakeCloudMetadataStore()
   286  	fakeCloudYamlFile := `
   287          clouds:
   288            fakecloud:
   289              type: maas
   290              auth-types: [oauth1, user-pass]
   291              endpoint: "http://garagemaas"`
   292  
   293  	cloudFile := prepareTestCloudYaml(c, fakeCloudYamlFile)
   294  	defer cloudFile.Close()
   295  	defer os.Remove(cloudFile.Name())
   296  
   297  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudFile.Name())
   298  	c.Assert(err, jc.ErrorIsNil)
   299  
   300  	fake.Call("ParseCloudMetadataFile", cloudFile.Name()).Returns(mockCloud, nil)
   301  
   302  	_, err = cmdtesting.RunCommand(c, cloud.NewAddCloudCommand(fake), "fakecloud", cloudFile.Name())
   303  	c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta(`auth type "user-pass" not supported`))
   304  }
   305  
   306  type fakeAddCloudAPI struct {
   307  	jujutesting.Stub
   308  }
   309  
   310  func (api *fakeAddCloudAPI) Close() error {
   311  	api.AddCall("Close", nil)
   312  	return nil
   313  }
   314  
   315  func (api *fakeAddCloudAPI) AddCloud(cloud jujucloud.Cloud) error {
   316  	api.AddCall("AddCloud", cloud)
   317  	return nil
   318  }
   319  
   320  func (api *fakeAddCloudAPI) AddCredential(tag string, credential jujucloud.Credential) error {
   321  	api.AddCall("AddCredential", tag, credential)
   322  	return nil
   323  }
   324  
   325  func (s *addSuite) setupControllerCloudScenario(c *gc.C) (
   326  	string, *cloud.AddCloudCommand, *jujuclient.MemStore, *fakeAddCloudAPI, jujucloud.Credential,
   327  ) {
   328  	cloudfile := prepareTestCloudYaml(c, garageMaasYamlFile)
   329  	s.AddCleanup(func(_ *gc.C) {
   330  		defer cloudfile.Close()
   331  		defer os.Remove(cloudfile.Name())
   332  	})
   333  
   334  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudfile.Name())
   335  	c.Assert(err, jc.ErrorIsNil)
   336  
   337  	fake := newFakeCloudMetadataStore()
   338  	fake.Call("ParseCloudMetadataFile", cloudfile.Name()).Returns(mockCloud, nil)
   339  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   340  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   341  	fake.Call("WritePersonalCloudMetadata", mockCloud).Returns(nil)
   342  
   343  	store := jujuclient.NewMemStore()
   344  	store.Controllers["mycontroller"] = jujuclient.ControllerDetails{}
   345  	store.Accounts["mycontroller"] = jujuclient.AccountDetails{User: "fred"}
   346  	cred := jujucloud.NewCredential(jujucloud.OAuth1AuthType, map[string]string{
   347  		"maas-oauth": "auth:token",
   348  	})
   349  	store.Credentials["garage-maas"] = jujucloud.CloudCredential{
   350  		AuthCredentials: map[string]jujucloud.Credential{"default": cred},
   351  	}
   352  
   353  	api := &fakeAddCloudAPI{}
   354  	cmd := cloud.NewAddCloudCommandForTest(fake, store, func() (cloud.AddCloudAPI, error) {
   355  		return api, nil
   356  	})
   357  	return cloudfile.Name(), cmd, store, api, cred
   358  }
   359  
   360  func (s *addSuite) TestAddToController(c *gc.C) {
   361  	cloudFileName, cmd, _, api, cred := s.setupControllerCloudScenario(c)
   362  	_, err := cmdtesting.RunCommand(
   363  		c, cmd, "garage-maas", cloudFileName, "-c", "mycontroller")
   364  	c.Assert(err, jc.ErrorIsNil)
   365  	api.CheckCallNames(c, "AddCloud", "AddCredential")
   366  	api.CheckCall(c, 0, "AddCloud", jujucloud.Cloud{
   367  		Name:        "garage-maas",
   368  		Type:        "maas",
   369  		Description: "Metal As A Service",
   370  		AuthTypes:   jujucloud.AuthTypes{"oauth1"},
   371  		Endpoint:    "http://garagemaas",
   372  	})
   373  	api.CheckCall(c, 1, "AddCredential", "cloudcred-garage-maas_fred_default", cred)
   374  }
   375  
   376  func (s *addSuite) TestAddToControllerBadController(c *gc.C) {
   377  	cloudFileName, cmd, store, _, _ := s.setupControllerCloudScenario(c)
   378  	store.Credentials = nil
   379  
   380  	_, err := cmdtesting.RunCommand(
   381  		c, cmd, "garage-maas", cloudFileName, "-c", "badcontroller")
   382  	c.Assert(err, gc.ErrorMatches, `controller badcontroller not found`)
   383  }
   384  
   385  func (s *addSuite) TestAddToControllerMissingCredential(c *gc.C) {
   386  	cloudFileName, cmd, store, _, _ := s.setupControllerCloudScenario(c)
   387  	store.Credentials = nil
   388  
   389  	_, err := cmdtesting.RunCommand(
   390  		c, cmd, "garage-maas", cloudFileName, "-c", "mycontroller")
   391  	c.Assert(err, gc.ErrorMatches, `loading credentials: credentials for cloud garage-maas not found`)
   392  }
   393  
   394  func (s *addSuite) TestAddToControllerAmbiguousCredential(c *gc.C) {
   395  	cloudFileName, cmd, store, _, cred := s.setupControllerCloudScenario(c)
   396  	store.Credentials["garage-maas"].AuthCredentials["another"] = cred
   397  
   398  	_, err := cmdtesting.RunCommand(
   399  		c, cmd, "garage-maas", cloudFileName, "-c", "mycontroller")
   400  	errMsg := strings.Replace(err.Error(), "\n", "", -1)
   401  	c.Assert(errMsg, gc.Matches, `.*more than one credential is available.*`)
   402  }
   403  
   404  func (*addSuite) TestInteractive(c *gc.C) {
   405  	command := cloud.NewAddCloudCommand(nil)
   406  	err := cmdtesting.InitCommand(command, nil)
   407  	c.Assert(err, jc.ErrorIsNil)
   408  
   409  	out := &bytes.Buffer{}
   410  
   411  	ctx := &cmd.Context{
   412  		Dir:    c.MkDir(),
   413  		Stdout: out,
   414  		Stderr: ioutil.Discard,
   415  		Stdin:  &bytes.Buffer{},
   416  	}
   417  	err = command.Run(ctx)
   418  	c.Check(errors.Cause(err), gc.Equals, io.EOF)
   419  
   420  	c.Assert(out.String(), gc.Equals, ""+
   421  		"Cloud Types\n"+
   422  		"  lxd\n"+
   423  		"  maas\n"+
   424  		"  manual\n"+
   425  		"  openstack\n"+
   426  		"  vsphere\n"+
   427  		"\n"+
   428  		"Select cloud type: \n",
   429  	)
   430  }
   431  
   432  func (*addSuite) TestInteractiveMaas(c *gc.C) {
   433  	fake := newFakeCloudMetadataStore()
   434  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   435  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   436  	const expectedYAMLarg = "" +
   437  		"auth-types:\n" +
   438  		"- oauth1\n" +
   439  		"endpoint: http://mymaas\n"
   440  	fake.Call("ParseOneCloud", []byte(expectedYAMLarg)).Returns(garageMAASCloud, nil)
   441  	m1Cloud := garageMAASCloud
   442  	m1Cloud.Name = "m1"
   443  	m1Metadata := map[string]jujucloud.Cloud{"m1": m1Cloud}
   444  	numCallsToWrite := fake.Call("WritePersonalCloudMetadata", m1Metadata).Returns(nil)
   445  
   446  	command := cloud.NewAddCloudCommandForTest(fake, jujuclient.NewMemStore(), nil)
   447  	err := cmdtesting.InitCommand(command, nil)
   448  	c.Assert(err, jc.ErrorIsNil)
   449  
   450  	ctx := &cmd.Context{
   451  		Stdout: ioutil.Discard,
   452  		Stderr: ioutil.Discard,
   453  		Stdin: strings.NewReader("" +
   454  			/* Select cloud type: */ "maas\n" +
   455  			/* Enter a name for the cloud: */ "m1\n" +
   456  			/* Enter the controller's hostname or IP address: */ "http://mymaas\n",
   457  		),
   458  	}
   459  
   460  	err = command.Run(ctx)
   461  	c.Assert(err, jc.ErrorIsNil)
   462  
   463  	c.Check(numCallsToWrite(), gc.Equals, 1)
   464  }
   465  
   466  func (*addSuite) TestInteractiveManual(c *gc.C) {
   467  	manCloud := manualCloud
   468  	manCloud.Name = "man"
   469  	fake := newFakeCloudMetadataStore()
   470  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   471  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   472  	fake.Call("ParseOneCloud", []byte("endpoint: 192.168.1.6\n")).Returns(manCloud, nil)
   473  	manMetadata := map[string]jujucloud.Cloud{"man": manCloud}
   474  	numCallsToWrite := fake.Call("WritePersonalCloudMetadata", manMetadata).Returns(nil)
   475  
   476  	command := cloud.NewAddCloudCommandForTest(fake, jujuclient.NewMemStore(), nil)
   477  	err := cmdtesting.InitCommand(command, nil)
   478  	c.Assert(err, jc.ErrorIsNil)
   479  
   480  	ctx := &cmd.Context{
   481  		Stdout: ioutil.Discard,
   482  		Stderr: ioutil.Discard,
   483  		Stdin: strings.NewReader("" +
   484  			/* Select cloud type: */ "manual\n" +
   485  			/* Enter a name for the cloud: */ "man\n" +
   486  			/* Enter the controller's hostname or IP address: */ "192.168.1.6\n",
   487  		),
   488  	}
   489  
   490  	err = command.Run(ctx)
   491  	c.Check(err, jc.ErrorIsNil)
   492  
   493  	c.Check(numCallsToWrite(), gc.Equals, 1)
   494  }
   495  
   496  func (*addSuite) TestInteractiveVSphere(c *gc.C) {
   497  	fake := newFakeCloudMetadataStore()
   498  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   499  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   500  	vsphereCloud := jujucloud.Cloud{
   501  		Name:      "mvs",
   502  		Type:      "vsphere",
   503  		AuthTypes: []jujucloud.AuthType{"userpass", "access-key"},
   504  		Endpoint:  "192.168.1.6",
   505  		Regions: []jujucloud.Region{
   506  			{
   507  				Name:     "foo",
   508  				Endpoint: "192.168.1.6",
   509  			},
   510  			{
   511  				Name:     "bar",
   512  				Endpoint: "192.168.1.6",
   513  			},
   514  		},
   515  	}
   516  	const expectedYAMLarg = "" +
   517  		"auth-types:\n" +
   518  		"- userpass\n" +
   519  		"endpoint: 192.168.1.6\n" +
   520  		"regions:\n" +
   521  		"  bar: {}\n" +
   522  		"  foo: {}\n"
   523  	fake.Call("ParseOneCloud", []byte(expectedYAMLarg)).Returns(vsphereCloud, nil)
   524  	vsphereMetadata := map[string]jujucloud.Cloud{"mvs": vsphereCloud}
   525  	numCallsToWrite := fake.Call("WritePersonalCloudMetadata", vsphereMetadata).Returns(nil)
   526  
   527  	command := cloud.NewAddCloudCommandForTest(fake, jujuclient.NewMemStore(), nil)
   528  	err := cmdtesting.InitCommand(command, nil)
   529  	c.Assert(err, jc.ErrorIsNil)
   530  
   531  	var stdout bytes.Buffer
   532  	ctx := &cmd.Context{
   533  		Stdout: &stdout,
   534  		Stderr: ioutil.Discard,
   535  		Stdin: strings.NewReader("" +
   536  			/* Select cloud type: */ "vsphere\n" +
   537  			/* Enter a name for the cloud: */ "mvs\n" +
   538  			/* Enter the vCenter address or URL: */ "192.168.1.6\n" +
   539  			/* Enter datacenter name: */ "foo\n" +
   540  			/* Enter another datacenter? (y/N): */ "y\n" +
   541  			/* Enter datacenter name: */ "bar\n" +
   542  			/* Enter another datacenter? (y/N): */ "n\n",
   543  		),
   544  	}
   545  
   546  	err = command.Run(ctx)
   547  	c.Check(err, jc.ErrorIsNil)
   548  
   549  	c.Check(numCallsToWrite(), gc.Equals, 1)
   550  	c.Check(stdout.String(), gc.Matches, "(.|\n)*"+`
   551  Select cloud type: 
   552  Enter a name for your vsphere cloud: 
   553  Enter the vCenter address or URL: 
   554  Enter datacenter name: 
   555  Enter another datacenter\? \(y/N\): 
   556  Enter datacenter name: 
   557  Enter another datacenter\? \(y/N\): 
   558  `[1:]+"(.|\n)*")
   559  }
   560  
   561  func (*addSuite) TestInteractiveExistingNameOverride(c *gc.C) {
   562  	manualCloud := manualCloud
   563  	manualCloud.Name = "homestack"
   564  
   565  	fake := newFakeCloudMetadataStore()
   566  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   567  	fake.Call("PersonalCloudMetadata").Returns(homestackMetadata(), nil)
   568  	manMetadata := map[string]jujucloud.Cloud{"homestack": manualCloud}
   569  	fake.Call("ParseOneCloud", []byte("endpoint: 192.168.1.6\n")).Returns(manualCloud, nil)
   570  	numCallsToWrite := fake.Call("WritePersonalCloudMetadata", manMetadata).Returns(nil)
   571  
   572  	command := cloud.NewAddCloudCommandForTest(fake, jujuclient.NewMemStore(), nil)
   573  	err := cmdtesting.InitCommand(command, nil)
   574  	c.Assert(err, jc.ErrorIsNil)
   575  
   576  	ctx := &cmd.Context{
   577  		Stdout: ioutil.Discard,
   578  		Stderr: ioutil.Discard,
   579  		Stdin: strings.NewReader("" +
   580  			/* Select cloud type: */ "manual\n" +
   581  			/* Enter a name for the cloud: */ "homestack\n" +
   582  			/* Do you want to replace that definition? */ "y\n" +
   583  			/* Enter the controller's hostname or IP address: */ "192.168.1.6\n",
   584  		),
   585  	}
   586  
   587  	err = command.Run(ctx)
   588  	c.Check(err, jc.ErrorIsNil)
   589  
   590  	c.Check(numCallsToWrite(), gc.Equals, 1)
   591  }
   592  
   593  func (*addSuite) TestInteractiveExistingNameNoOverride(c *gc.C) {
   594  	fake := newFakeCloudMetadataStore()
   595  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   596  	fake.Call("PersonalCloudMetadata").Returns(homestackMetadata(), nil)
   597  	homestack2Cloud := jujucloud.Cloud{
   598  		Name:     "homestack2",
   599  		Type:     "manual",
   600  		Endpoint: "192.168.1.6",
   601  	}
   602  	fake.Call("ParseOneCloud", []byte("endpoint: 192.168.1.6\n")).Returns(homestack2Cloud, nil)
   603  	compoundCloudMetadata := map[string]jujucloud.Cloud{
   604  		"homestack":  homestackCloud,
   605  		"homestack2": homestack2Cloud,
   606  	}
   607  	numCallsToWrite := fake.Call("WritePersonalCloudMetadata", compoundCloudMetadata).Returns(nil)
   608  
   609  	command := cloud.NewAddCloudCommandForTest(fake, jujuclient.NewMemStore(), nil)
   610  	err := cmdtesting.InitCommand(command, nil)
   611  	c.Assert(err, jc.ErrorIsNil)
   612  
   613  	var out bytes.Buffer
   614  	ctx := &cmd.Context{
   615  		Stdout: &out,
   616  		Stderr: ioutil.Discard,
   617  		Stdin: strings.NewReader("" +
   618  			/* Select cloud type: */ "manual\n" +
   619  			/* Enter a name for the cloud: */ "homestack" + "\n" +
   620  			/* Do you want to replace that definition? (y/N): */ "n\n" +
   621  			/* Enter a name for the cloud: */ "homestack2" + "\n" +
   622  			/* Enter the controller's hostname or IP address: */ "192.168.1.6" + "\n",
   623  		),
   624  	}
   625  
   626  	err = command.Run(ctx)
   627  	c.Log(out.String())
   628  	c.Assert(err, jc.ErrorIsNil)
   629  
   630  	c.Check(numCallsToWrite(), gc.Equals, 1)
   631  }
   632  
   633  func (*addSuite) TestInteractiveAddCloud_PromptForNameIsCorrect(c *gc.C) {
   634  	var out bytes.Buffer
   635  	ctx := &cmd.Context{
   636  		Stdout: &out,
   637  		Stderr: ioutil.Discard,
   638  		Stdin: strings.NewReader("" +
   639  			/* Select cloud type: */ "manual\n",
   640  		),
   641  	}
   642  
   643  	fake := newFakeCloudMetadataStore()
   644  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   645  	fake.Call("PersonalCloudMetadata").Returns(homestackMetadata(), nil)
   646  
   647  	command := cloud.NewAddCloudCommand(fake)
   648  	// Running the command will return an error because we only give
   649  	// enough input to get to the prompt we care about checking. This
   650  	// test ignores this error.
   651  	err := command.Run(ctx)
   652  	c.Assert(errors.Cause(err), gc.Equals, io.EOF)
   653  
   654  	c.Check(out.String(), gc.Matches, "(?s).+Enter a name for your manual cloud: .*")
   655  }
   656  
   657  func (*addSuite) TestSpecifyingjujucloudThroughFlag_CorrectlySetsMemberVar(c *gc.C) {
   658  	command := cloud.NewAddCloudCommand(nil)
   659  	runCmd := func() {
   660  		cmdtesting.RunCommand(c, command, "garage-maas", "-f", "fake.yaml")
   661  	}
   662  	c.Assert(runCmd, gc.PanicMatches, "runtime error: invalid memory address or nil pointer dereference")
   663  	//c.Check(command.jujucloud, gc.Equals, "fake.yaml")
   664  }
   665  
   666  func (*addSuite) TestSpecifyingjujucloudThroughFlagAndArgument_Errors(c *gc.C) {
   667  	command := cloud.NewAddCloudCommand(nil)
   668  	_, err := cmdtesting.RunCommand(c, command, "garage-maas", "-f", "fake.yaml", "foo.yaml")
   669  	c.Check(err, gc.ErrorMatches, "cannot specify cloud file with option and argument")
   670  }
   671  
   672  func (*addSuite) TestValidateGoodCloudFile(c *gc.C) {
   673  	data := `
   674  clouds:
   675    foundations:
   676      type: maas
   677      auth-types: [oauth1]
   678      endpoint: "http://10.245.31.100/MAAS"`
   679  
   680  	cloudFile := prepareTestCloudYaml(c, data)
   681  	defer cloudFile.Close()
   682  	defer os.Remove(cloudFile.Name())
   683  
   684  	var logWriter loggo.TestWriter
   685  	writerName := "add_cloud_tests_writer"
   686  	c.Assert(loggo.RegisterWriter(writerName, &logWriter), jc.ErrorIsNil)
   687  	defer func() {
   688  		loggo.RemoveWriter(writerName)
   689  		logWriter.Clear()
   690  	}()
   691  
   692  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudFile.Name())
   693  	c.Assert(err, jc.ErrorIsNil)
   694  
   695  	fake := newFakeCloudMetadataStore()
   696  	fake.Call("ParseCloudMetadataFile", cloudFile.Name()).Returns(mockCloud, nil)
   697  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   698  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   699  	fake.Call("WritePersonalCloudMetadata", mockCloud).Returns(nil)
   700  
   701  	command := cloud.NewAddCloudCommand(fake)
   702  
   703  	_, err = cmdtesting.RunCommand(c, command, "foundations", cloudFile.Name())
   704  	c.Check(err, jc.ErrorIsNil)
   705  
   706  	c.Check(logWriter.Log(), jc.LogMatches, []jc.SimpleMessage{})
   707  }
   708  
   709  func (*addSuite) TestValidateBadjujucloud(c *gc.C) {
   710  	data := `
   711  clouds:
   712    foundations:
   713      type: maas
   714      auth-typs: [oauth1]
   715      endpoint: "http://10.245.31.100/MAAS"`
   716  
   717  	cloudFile := prepareTestCloudYaml(c, data)
   718  	defer cloudFile.Close()
   719  	defer os.Remove(cloudFile.Name())
   720  
   721  	mockCloud, err := jujucloud.ParseCloudMetadataFile(cloudFile.Name())
   722  	c.Assert(err, jc.ErrorIsNil)
   723  
   724  	fake := newFakeCloudMetadataStore()
   725  	fake.Call("ParseCloudMetadataFile", cloudFile.Name()).Returns(mockCloud, nil)
   726  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   727  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   728  	fake.Call("WritePersonalCloudMetadata", mockCloud).Returns(nil)
   729  
   730  	command := cloud.NewAddCloudCommand(fake)
   731  
   732  	var logWriter loggo.TestWriter
   733  	writerName := "add_cloud_tests_writer"
   734  	c.Assert(loggo.RegisterWriter(writerName, &logWriter), jc.ErrorIsNil)
   735  	defer func() {
   736  		loggo.RemoveWriter(writerName)
   737  		logWriter.Clear()
   738  	}()
   739  
   740  	_, err = cmdtesting.RunCommand(c, command, "foundations", cloudFile.Name())
   741  	c.Check(err, jc.ErrorIsNil)
   742  
   743  	c.Check(logWriter.Log(), jc.LogMatches, []jc.SimpleMessage{
   744  		{
   745  			Level:   loggo.WARNING,
   746  			Message: `property "auth-typs" is invalid. Perhaps you mean "auth-types".`,
   747  		},
   748  	})
   749  }
   750  
   751  func prepareTestCloudYaml(c *gc.C, data string) *os.File {
   752  	jujucloud, err := ioutil.TempFile("", "jujucloud")
   753  	c.Assert(err, jc.ErrorIsNil)
   754  
   755  	err = ioutil.WriteFile(jujucloud.Name(), []byte(data), 0644)
   756  	if err != nil {
   757  		jujucloud.Close()
   758  		os.Remove(jujucloud.Name())
   759  		c.Fatal(err.Error())
   760  	}
   761  
   762  	return jujucloud
   763  }
   764  
   765  func (s *addSuite) TestInvalidCredentialMessage(c *gc.C) {
   766  	fake := newFakeCloudMetadataStore()
   767  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   768  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   769  	const expectedYAMLarg = "" +
   770  		"auth-types:\n" +
   771  		"- oauth1\n" +
   772  		"endpoint: http://mymaas\n"
   773  	fake.Call("ParseOneCloud", []byte(expectedYAMLarg)).Returns(garageMAASCloud, nil)
   774  	m1Cloud := garageMAASCloud
   775  	m1Cloud.Name = "m1"
   776  	m1Metadata := map[string]jujucloud.Cloud{"m1": m1Cloud}
   777  	fake.Call("WritePersonalCloudMetadata", m1Metadata).Returns(nil)
   778  
   779  	command := cloud.NewAddCloudCommandForTest(fake, jujuclient.NewMemStore(), nil)
   780  	command.Ping = func(environs.EnvironProvider, string) error {
   781  		return command.CloudCallCtx.InvalidateCredential("running test")
   782  	}
   783  
   784  	ctx := cmdtesting.Context(c)
   785  	ctx.Stdin = strings.NewReader("" +
   786  		/* Select cloud type: */ "maas\n" +
   787  		/* Enter a name for the cloud: */ "m1\n" +
   788  		/* Enter the controller's hostname or IP address: */ "http://mymaas\n",
   789  	)
   790  
   791  	err := command.Run(ctx)
   792  	c.Assert(err, jc.ErrorIsNil)
   793  	c.Assert(cmdtesting.Stderr(ctx), jc.Contains, "Cloud credential is not accepted by cloud provider: running test")
   794  }
   795  
   796  func (*addSuite) TestInteractiveOpenstackNoCloudCert(c *gc.C) {
   797  	myOpenstack := jujucloud.Cloud{
   798  		Name:      "os1",
   799  		Type:      "openstack",
   800  		AuthTypes: []jujucloud.AuthType{"userpass", "access-key"},
   801  		Endpoint:  "http://myopenstack",
   802  		Regions: []jujucloud.Region{
   803  			{
   804  				Name:     "regionone",
   805  				Endpoint: "http://boston/1.0",
   806  			},
   807  		},
   808  	}
   809  
   810  	var expectedYAMLarg = "" +
   811  		"auth-types:\n" +
   812  		"- userpass\n" +
   813  		"- access-key\n" +
   814  		"certfilename: \"\"\n" +
   815  		"endpoint: http://myopenstack\n" +
   816  		"regions:\n" +
   817  		"  regionone:\n" +
   818  		"    endpoint: http://boston/1.0\n"
   819  
   820  	var input = "" +
   821  		/* Select cloud type: */ "openstack\n" +
   822  		/* Enter a name for your openstack cloud: */ "os1\n" +
   823  		/* Enter the API endpoint url for the cloud []: */ "http://myopenstack\n" +
   824  		/* Enter ta path to the CA certificate for your cloud if one is required to access it. (optional) [none] */ "\n" +
   825  		/* Select one or more auth types separated by commas: */ "userpass,access-key\n" +
   826  		/* Enter region name: */ "regionone\n" +
   827  		/* Enter the API endpoint url for the region [use cloud api url]: */ "http://boston/1.0\n" +
   828  		/* Enter another region? (Y/n): */ "n\n"
   829  
   830  	testInteractiveOpenstack(c, myOpenstack, expectedYAMLarg, input, "", "")
   831  }
   832  
   833  // Note: The first %s is filled with a string containing a newline
   834  var expectedCloudYAMLarg = `
   835  auth-types:
   836  - userpass
   837  - access-key
   838  %scertfilename: %s
   839  endpoint: http://myopenstack
   840  regions:
   841    regionone:
   842      endpoint: ""
   843  `[1:]
   844  
   845  func (*addSuite) TestInteractiveOpenstackCloudCertFail(c *gc.C) {
   846  	fakeCertDir := c.MkDir()
   847  	fakeCertFilename := path.Join(fakeCertDir, "cloudcert.crt")
   848  
   849  	invalidCertFilename := path.Join(fakeCertDir, "invalid.crt")
   850  	ioutil.WriteFile(invalidCertFilename, []byte("testing certification validation"), 0666)
   851  
   852  	input := fmt.Sprintf(""+
   853  		/* Select cloud type: */ "openstack\n"+
   854  		/* Enter a name for your openstack cloud: */ "os1\n"+
   855  		/* Enter the API endpoint url for the cloud []: */ "http://myopenstack\n"+
   856  		/* Enter a path to the CA certificate for your cloud if one is required to access it. (optional) [none] */ "%s\n"+
   857  		/* Enter a path to the CA certificate for your cloud if one is required to access it. (optional) [none] */ "%s\n"+
   858  		/* Select one or more auth types separated by commas: */ "userpass,access-key\n"+
   859  		/* Enter region name: */ "regionone\n"+
   860  		/* Enter the API endpoint url for the region [use cloud api url]: */ "\n"+
   861  		/* Enter another region? (Y/n): */ "n\n", invalidCertFilename, fakeCertFilename)
   862  
   863  	testInteractiveOpenstackCloudCert(c, fakeCertFilename, input,
   864  		fmt.Sprintf("Successfully read CA Certificate from %s\n", fakeCertFilename),
   865  		fmt.Sprintf("Can't validate CA Certificate %s: no certificates found", invalidCertFilename))
   866  }
   867  
   868  func (*addSuite) TestInteractiveOpenstackCloudCertReadFailRetry(c *gc.C) {
   869  	var invalidCertFilename = "/tmp/no-such-file"
   870  	fakeCertDir := c.MkDir()
   871  	fakeCertFilename := path.Join(fakeCertDir, "cloudcert.crt")
   872  
   873  	input := fmt.Sprintf(""+
   874  		/* Select cloud type: */ "openstack\n"+
   875  		/* Enter a name for your openstack cloud: */ "os1\n"+
   876  		/* Enter the API endpoint url for the cloud []: */ "http://myopenstack\n"+
   877  		/* Enter a path to the CA certificate for your cloud if one is required to access it. (optional) [none] */ "%s\n"+
   878  		/* Enter a path to the CA certificate for your cloud if one is required to access it. (optional) [none] */ "%s\n"+
   879  		/* Select one or more auth types separated by commas: */ "userpass,access-key\n"+
   880  		/* Enter region name: */ "regionone\n"+
   881  		/* Enter the API endpoint url for the region [use cloud api url]: */ "\n"+
   882  		/* Enter another region? (Y/n): */ "n\n", invalidCertFilename, fakeCertFilename)
   883  
   884  	testInteractiveOpenstackCloudCert(c,
   885  		fakeCertFilename,
   886  		input,
   887  		fmt.Sprintf("Successfully read CA Certificate from %s\n", fakeCertFilename),
   888  		fmt.Sprintf("Can't validate CA Certificate file: open %s:", invalidCertFilename),
   889  	)
   890  }
   891  
   892  func (*addSuite) TestInteractiveOpenstackCloudCert(c *gc.C) {
   893  	fakeCertFilename := path.Join(c.MkDir(), "cloudcert.crt")
   894  
   895  	input := fmt.Sprintf(""+
   896  		/* Select cloud type: */ "openstack\n"+
   897  		/* Enter a name for your openstack cloud: */ "os1\n"+
   898  		/* Enter the API endpoint url for the cloud []: */ "http://myopenstack\n"+
   899  		/* Enter a path to the CA certificate for your cloud if one is required to access it. (optional) [none] */ "%s\n"+
   900  		/* Select one or more auth types separated by commas: */ "userpass,access-key\n"+
   901  		/* Enter region name: */ "regionone\n"+
   902  		/* Enter the API endpoint url for the region [use cloud api url]: */ "\n"+
   903  		/* Enter another region? (Y/n): */ "n\n", fakeCertFilename)
   904  
   905  	testInteractiveOpenstackCloudCert(c, fakeCertFilename, input,
   906  		fmt.Sprintf("Successfully read CA Certificate from %s\n", fakeCertFilename), "")
   907  }
   908  
   909  type addOpenStackSuite struct {
   910  	jujutesting.IsolationSuite
   911  }
   912  
   913  var _ = gc.Suite(&addOpenStackSuite{})
   914  
   915  func (s *addOpenStackSuite) TearDownTest(c *gc.C) {
   916  	s.IsolationSuite.TearDownTest(c)
   917  	os.Unsetenv("OS_CACERT")
   918  	os.Unsetenv("OS_AUTH_URL")
   919  }
   920  
   921  func (*addOpenStackSuite) TestInteractiveOpenstackCloudCertEnvVar(c *gc.C) {
   922  	fakeCertFilename := path.Join(c.MkDir(), "cloudcert.crt")
   923  
   924  	input := "" +
   925  		/* Select cloud type: */ "openstack\n" +
   926  		/* Enter a name for your openstack cloud: */ "os1\n" +
   927  		/* Enter the API endpoint url for the cloud [$OS_AUTH_URL]: */ "\n" +
   928  		/* Enter a path to the CA certificate for your cloud if one is required to access it. (optional) [$OS_CACERT] */ "\n" +
   929  		/* Select one or more auth types separated by commas: */ "userpass,access-key\n" +
   930  		/* Enter region name: */ "regionone\n" +
   931  		/* Enter the API endpoint url for the region [use cloud api url]: */ "\n" +
   932  		/* Enter another region? (Y/n): */ "n\n"
   933  
   934  	os.Setenv("OS_CACERT", fakeCertFilename)
   935  	os.Setenv("OS_AUTH_URL", "http://myopenstack")
   936  
   937  	testInteractiveOpenstackCloudCert(c, fakeCertFilename, input,
   938  		fmt.Sprintf("Successfully read CA Certificate from %s\n", fakeCertFilename), "")
   939  }
   940  
   941  func testInteractiveOpenstackCloudCert(c *gc.C, fakeCertFilename, input, addStdErrMsg, stdOutMsg string) {
   942  	fakeCert := testing.CACert
   943  	ioutil.WriteFile(fakeCertFilename, []byte(fakeCert), 0666)
   944  
   945  	myOpenstack := jujucloud.Cloud{
   946  		Name:      "os1",
   947  		Type:      "openstack",
   948  		AuthTypes: []jujucloud.AuthType{"userpass", "access-key"},
   949  		Endpoint:  "http://myopenstack",
   950  		Regions: []jujucloud.Region{
   951  			{
   952  				Name:     "regionone",
   953  				Endpoint: "http://myopenstack",
   954  			},
   955  		},
   956  		CACertificates: []string{fakeCert},
   957  	}
   958  
   959  	fakeCertMap := map[string]interface{}{
   960  		"ca-certificates": []string{fakeCert},
   961  	}
   962  	fakeCertYaml, err := yaml.Marshal(fakeCertMap)
   963  	c.Assert(err, gc.IsNil)
   964  
   965  	expectedYAMLarg := fmt.Sprintf(expectedCloudYAMLarg, fakeCertYaml, fakeCertFilename)
   966  
   967  	testInteractiveOpenstack(c, myOpenstack, expectedYAMLarg, input, addStdErrMsg, stdOutMsg)
   968  }
   969  
   970  func testInteractiveOpenstack(c *gc.C, myOpenstack jujucloud.Cloud, expectedYAMLarg, input, addStdErrMsg, stdOutMsg string) {
   971  	fake := newFakeCloudMetadataStore()
   972  	fake.Call("PublicCloudMetadata", []string(nil)).Returns(map[string]jujucloud.Cloud{}, false, nil)
   973  	fake.Call("PersonalCloudMetadata").Returns(map[string]jujucloud.Cloud{}, nil)
   974  
   975  	fake.Call("ParseOneCloud", []byte(expectedYAMLarg)).Returns(myOpenstack, nil)
   976  	m1Metadata := map[string]jujucloud.Cloud{"os1": myOpenstack}
   977  	numCallsToWrite := fake.Call("WritePersonalCloudMetadata", m1Metadata).Returns(nil)
   978  
   979  	command := cloud.NewAddCloudCommandForTest(fake, jujuclient.NewMemStore(), nil)
   980  	err := cmdtesting.InitCommand(command, nil)
   981  	c.Assert(err, jc.ErrorIsNil)
   982  
   983  	ctx := cmdtesting.Context(c)
   984  	ctx.Stdin = strings.NewReader(input)
   985  
   986  	err = command.Run(ctx)
   987  
   988  	if err != nil {
   989  		fmt.Printf("expectedYAML\n(%s)\n", expectedYAMLarg)
   990  	}
   991  
   992  	c.Check(err, jc.ErrorIsNil)
   993  	var output = addStdErrMsg +
   994  		"Cloud \"os1\" successfully added\n" +
   995  		"\n" +
   996  		"You will need to add credentials for this cloud (`juju add-credential os1`)\n" +
   997  		"before creating a controller (`juju bootstrap os1`).\n"
   998  	c.Assert(cmdtesting.Stderr(ctx), jc.Contains, output)
   999  	c.Assert(cmdtesting.Stdout(ctx), jc.Contains, stdOutMsg)
  1000  
  1001  	c.Check(numCallsToWrite(), gc.Equals, 1)
  1002  }