Example #1
0
File: charms.go Project: bac/juju
// processGet handles a charm file GET request after authentication.
// It returns the bundle path, the requested file path (if any), whether the
// default charm icon has been requested and an error.
func (h *charmsHandler) processGet(r *http.Request, st *state.State) (
	archivePath string,
	fileArg string,
	serveIcon bool,
	err error,
) {
	errRet := func(err error) (string, string, bool, error) {
		return "", "", false, err
	}

	query := r.URL.Query()

	// Retrieve and validate query parameters.
	curlString := query.Get("url")
	if curlString == "" {
		return errRet(errors.Errorf("expected url=CharmURL query argument"))
	}
	curl, err := charm.ParseURL(curlString)
	if err != nil {
		return errRet(errors.Trace(err))
	}
	fileArg = query.Get("file")
	if fileArg != "" {
		fileArg = path.Clean(fileArg)
	} else if query.Get("icon") == "1" {
		serveIcon = true
		fileArg = "icon.svg"
	}

	// Ensure the working directory exists.
	tmpDir := filepath.Join(h.dataDir, "charm-get-tmp")
	if err = os.MkdirAll(tmpDir, 0755); err != nil {
		return errRet(errors.Annotate(err, "cannot create charms tmp directory"))
	}

	// Use the storage to retrieve and save the charm archive.
	storage := storage.NewStorage(st.ModelUUID(), st.MongoSession())
	ch, err := st.Charm(curl)
	if err != nil {
		return errRet(errors.Annotate(err, "cannot get charm from state"))
	}

	reader, _, err := storage.Get(ch.StoragePath())
	if err != nil {
		return errRet(errors.Annotate(err, "cannot get charm from model storage"))
	}
	defer reader.Close()

	charmFile, err := ioutil.TempFile(tmpDir, "charm")
	if err != nil {
		return errRet(errors.Annotate(err, "cannot create charm archive file"))
	}
	if _, err = io.Copy(charmFile, reader); err != nil {
		cleanupFile(charmFile)
		return errRet(errors.Annotate(err, "error processing charm archive download"))
	}

	charmFile.Close()
	return charmFile.Name(), fileArg, serveIcon, nil
}
Example #2
0
// StoreCharmArchive stores a charm archive in environment storage.
func StoreCharmArchive(st *state.State, curl *charm.URL, ch charm.Charm, r io.Reader, size int64, sha256 string) error {
	storage := newStateStorage(st.ModelUUID(), st.MongoSession())
	storagePath, err := charmArchiveStoragePath(curl)
	if err != nil {
		return errors.Annotate(err, "cannot generate charm archive name")
	}
	if err := storage.Put(storagePath, r, size); err != nil {
		return errors.Annotate(err, "cannot add charm to storage")
	}

	// Now update the charm data in state and mark it as no longer pending.
	_, err = st.UpdateUploadedCharm(ch, curl, storagePath, sha256)
	if err != nil {
		alreadyUploaded := err == state.ErrCharmRevisionAlreadyModified ||
			errors.Cause(err) == state.ErrCharmRevisionAlreadyModified ||
			state.IsCharmAlreadyUploadedError(err)
		if err := storage.Remove(storagePath); err != nil {
			if alreadyUploaded {
				logger.Errorf("cannot remove duplicated charm archive from storage: %v", err)
			} else {
				logger.Errorf("cannot remove unsuccessfully recorded charm archive from storage: %v", err)
			}
		}
		if alreadyUploaded {
			// Somebody else managed to upload and update the charm in
			// state before us. This is not an error.
			return nil
		}
	}
	return nil
}
Example #3
0
// TestingApiHandler gives you an ApiHandler that isn't connected to
// anything real. It's enough to let test some basic functionality though.
func TestingApiHandler(c *gc.C, srvSt, st *state.State) (*apiHandler, *common.Resources) {
	srv := &Server{
		state: srvSt,
		tag:   names.NewMachineTag("0"),
	}
	h, err := newApiHandler(srv, st, nil, nil, st.ModelUUID())
	c.Assert(err, jc.ErrorIsNil)
	return h, h.getResources()
}
Example #4
0
// dyingEnvWorker is passed to NewModelWorkerManager in these tests. It
// creates a fake Runner instance when envWorkerManager starts workers for a
// dying or dead environment.
func (s *suite) dyingEnvWorker(ssSt modelworkermanager.InitialState, st *state.State) (worker.Worker, error) {
	if s.startErr != nil {
		return nil, s.startErr
	}
	runner := &fakeRunner{
		ssModelUUID: ssSt.ModelUUID(),
		modelUUID:   st.ModelUUID(),
	}
	s.runnerC <- runner
	return runner, nil
}
Example #5
0
// getSecretKeyLoginResponsePayload returns the information required by the
// client to login to the controller securely.
func (h *registerUserHandler) getSecretKeyLoginResponsePayload(
	st *state.State,
) (*params.SecretKeyLoginResponsePayload, error) {
	if !st.IsController() {
		return nil, errors.New("state is not for a controller")
	}
	payload := params.SecretKeyLoginResponsePayload{
		CACert:         st.CACert(),
		ControllerUUID: st.ModelUUID(),
	}
	return &payload, nil
}
Example #6
0
// TestingAPIHandler gives you an APIHandler that isn't connected to
// anything real. It's enough to let test some basic functionality though.
func TestingAPIHandler(c *gc.C, srvSt, st *state.State) (*apiHandler, *common.Resources) {
	authCtxt, err := newAuthContext(srvSt)
	c.Assert(err, jc.ErrorIsNil)
	srv := &Server{
		authCtxt: authCtxt,
		state:    srvSt,
		tag:      names.NewMachineTag("0"),
	}
	h, err := newAPIHandler(srv, st, nil, st.ModelUUID(), "testing.invalid:1234")
	c.Assert(err, jc.ErrorIsNil)
	return h, h.getResources()
}
Example #7
0
func addCharm(st *state.State, curl *charm.URL, ch charm.Charm) (*state.Charm, error) {
	var f *os.File
	name := charm.Quote(curl.String())
	switch ch := ch.(type) {
	case *charm.CharmDir:
		var err error
		if f, err = ioutil.TempFile("", name); err != nil {
			return nil, err
		}
		defer os.Remove(f.Name())
		defer f.Close()
		err = ch.ArchiveTo(f)
		if err != nil {
			return nil, fmt.Errorf("cannot bundle charm: %v", err)
		}
		if _, err := f.Seek(0, 0); err != nil {
			return nil, err
		}
	case *charm.CharmArchive:
		var err error
		if f, err = os.Open(ch.Path); err != nil {
			return nil, fmt.Errorf("cannot read charm bundle: %v", err)
		}
		defer f.Close()
	default:
		return nil, fmt.Errorf("unknown charm type %T", ch)
	}
	digest, size, err := utils.ReadSHA256(f)
	if err != nil {
		return nil, err
	}
	if _, err := f.Seek(0, 0); err != nil {
		return nil, err
	}

	stor := statestorage.NewStorage(st.ModelUUID(), st.MongoSession())
	storagePath := fmt.Sprintf("/charms/%s-%s", curl.String(), digest)
	if err := stor.Put(storagePath, f, size); err != nil {
		return nil, fmt.Errorf("cannot put charm: %v", err)
	}
	info := state.CharmInfo{
		Charm:       ch,
		ID:          curl,
		StoragePath: storagePath,
		SHA256:      digest,
	}
	sch, err := st.AddCharm(info)
	if err != nil {
		return nil, fmt.Errorf("cannot add charm: %v", err)
	}
	return sch, nil
}
Example #8
0
func (s *imageSuite) storeFakeImage(c *gc.C, st *state.State, kind, series, arch string) {
	storage := st.ImageStorage()
	metadata := &imagestorage.Metadata{
		ModelUUID: st.ModelUUID(),
		Kind:      kind,
		Series:    series,
		Arch:      arch,
		Size:      int64(len(testImageData)),
		SHA256:    testImageChecksum,
		SourceURL: "http://path",
	}
	err := storage.AddImage(strings.NewReader(testImageData), metadata)
	c.Assert(err, gc.IsNil)
}
Example #9
0
// NewModelManagerAPI creates a new api server endpoint for managing
// models.
func NewModelManagerAPI(
	st *state.State,
	resources *common.Resources,
	authorizer common.Authorizer,
) (*ModelManagerAPI, error) {
	if !authorizer.AuthClient() {
		return nil, common.ErrPerm
	}

	urlGetter := common.NewToolsURLGetter(st.ModelUUID(), st)
	return &ModelManagerAPI{
		state:       getState(st),
		authorizer:  authorizer,
		toolsFinder: common.NewToolsFinder(st, st, urlGetter),
	}, nil
}
Example #10
0
// getSecretKeyLoginResponsePayload returns the information required by the
// client to login to the controller securely.
func (h *registerUserHandler) getSecretKeyLoginResponsePayload(
	st *state.State, userTag names.UserTag,
) (*params.SecretKeyLoginResponsePayload, error) {
	if !st.IsController() {
		return nil, errors.New("state is not for a controller")
	}
	mac, err := h.createLocalLoginMacaroon(userTag)
	if err != nil {
		return nil, errors.Trace(err)
	}
	payload := params.SecretKeyLoginResponsePayload{
		CACert:         st.CACert(),
		ControllerUUID: st.ModelUUID(),
		Macaroon:       mac,
	}
	return &payload, nil
}
Example #11
0
// newBakeryService creates a new bakery.Service.
func newBakeryService(
	st *state.State,
	store bakerystorage.ExpirableStorage,
	locator bakery.PublicKeyLocator,
) (*bakery.Service, *bakery.KeyPair, error) {
	key, err := bakery.GenerateKey()
	if err != nil {
		return nil, nil, errors.Annotate(err, "generating key for bakery service")
	}
	service, err := bakery.NewService(bakery.NewServiceParams{
		Location: "juju model " + st.ModelUUID(),
		Store:    store,
		Key:      key,
		Locator:  locator,
	})
	if err != nil {
		return nil, nil, errors.Trace(err)
	}
	return service, key, nil
}
Example #12
0
// processGet handles a charm file GET request after authentication.
// It returns the bundle path, the requested file path (if any) and an error.
func (h *charmsHandler) processGet(r *http.Request, st *state.State) (string, string, error) {
	query := r.URL.Query()

	// Retrieve and validate query parameters.
	curlString := query.Get("url")
	if curlString == "" {
		return "", "", fmt.Errorf("expected url=CharmURL query argument")
	}
	curl, err := charm.ParseURL(curlString)
	if err != nil {
		return "", "", errors.Annotate(err, "cannot parse charm URL")
	}

	var filePath string
	file := query.Get("file")
	if file == "" {
		filePath = ""
	} else {
		filePath = path.Clean(file)
	}

	// Prepare the bundle directories.
	name := charm.Quote(curlString)
	charmArchivePath := filepath.Join(
		h.dataDir,
		"charm-get-cache",
		st.ModelUUID(),
		name+".zip",
	)

	// Check if the charm archive is already in the cache.
	if _, err := os.Stat(charmArchivePath); os.IsNotExist(err) {
		// Download the charm archive and save it to the cache.
		if err = h.downloadCharm(st, curl, charmArchivePath); err != nil {
			return "", "", errors.Annotate(err, "unable to retrieve and save the charm")
		}
	} else if err != nil {
		return "", "", errors.Annotate(err, "cannot access the charms cache")
	}
	return charmArchivePath, filePath, nil
}
Example #13
0
// downloadCharm downloads the given charm name from the provider storage and
// saves the corresponding zip archive to the given charmArchivePath.
func (h *charmsHandler) downloadCharm(st *state.State, curl *charm.URL, charmArchivePath string) error {
	storage := storage.NewStorage(st.ModelUUID(), st.MongoSession())
	ch, err := st.Charm(curl)
	if err != nil {
		return errors.Annotate(err, "cannot get charm from state")
	}

	// In order to avoid races, the archive is saved in a temporary file which
	// is then atomically renamed. The temporary file is created in the
	// charm cache directory so that we can safely assume the rename source and
	// target live in the same file system.
	cacheDir := filepath.Dir(charmArchivePath)
	if err = os.MkdirAll(cacheDir, 0755); err != nil {
		return errors.Annotate(err, "cannot create the charms cache")
	}
	tempCharmArchive, err := ioutil.TempFile(cacheDir, "charm")
	if err != nil {
		return errors.Annotate(err, "cannot create charm archive temp file")
	}
	defer tempCharmArchive.Close()

	// Use the storage to retrieve and save the charm archive.
	reader, _, err := storage.Get(ch.StoragePath())
	if err != nil {
		defer cleanupFile(tempCharmArchive)
		return errors.Annotate(err, "cannot get charm from model storage")
	}
	defer reader.Close()

	if _, err = io.Copy(tempCharmArchive, reader); err != nil {
		defer cleanupFile(tempCharmArchive)
		return errors.Annotate(err, "error processing charm archive download")
	}
	tempCharmArchive.Close()
	if err = os.Rename(tempCharmArchive.Name(), charmArchivePath); err != nil {
		defer cleanupFile(tempCharmArchive)
		return errors.Annotate(err, "error renaming the charm archive")
	}
	return nil
}
Example #14
0
// StoreCharmArchive stores a charm archive in environment storage.
func StoreCharmArchive(st *state.State, archive CharmArchive) error {
	storage := newStateStorage(st.ModelUUID(), st.MongoSession())
	storagePath, err := charmArchiveStoragePath(archive.ID)
	if err != nil {
		return errors.Annotate(err, "cannot generate charm archive name")
	}
	if err := storage.Put(storagePath, archive.Data, archive.Size); err != nil {
		return errors.Annotate(err, "cannot add charm to storage")
	}

	info := state.CharmInfo{
		Charm:       archive.Charm,
		ID:          archive.ID,
		StoragePath: storagePath,
		SHA256:      archive.SHA256,
		Macaroon:    archive.Macaroon,
	}

	// Now update the charm data in state and mark it as no longer pending.
	_, err = st.UpdateUploadedCharm(info)
	if err != nil {
		alreadyUploaded := err == state.ErrCharmRevisionAlreadyModified ||
			errors.Cause(err) == state.ErrCharmRevisionAlreadyModified ||
			state.IsCharmAlreadyUploadedError(err)
		if err := storage.Remove(storagePath); err != nil {
			if alreadyUploaded {
				logger.Errorf("cannot remove duplicated charm archive from storage: %v", err)
			} else {
				logger.Errorf("cannot remove unsuccessfully recorded charm archive from storage: %v", err)
			}
		}
		if alreadyUploaded {
			// Somebody else managed to upload and update the charm in
			// state before us. This is not an error.
			return nil
		}
	}
	return nil
}
Example #15
0
// newMacaroonAuth returns an authenticator that can authenticate
// macaroon-based logins. This is just a helper function for authCtxt.macaroonAuth.
func newMacaroonAuth(st *state.State) (*authentication.MacaroonAuthenticator, error) {
	envCfg, err := st.ModelConfig()
	if err != nil {
		return nil, errors.Annotate(err, "cannot get model config")
	}
	idURL := envCfg.IdentityURL()
	if idURL == "" {
		return nil, errMacaroonAuthNotConfigured
	}
	// The identity server has been configured,
	// so configure the bakery service appropriately.
	idPK := envCfg.IdentityPublicKey()
	if idPK == nil {
		// No public key supplied - retrieve it from the identity manager.
		idPK, err = httpbakery.PublicKeyForLocation(http.DefaultClient, idURL)
		if err != nil {
			return nil, errors.Annotate(err, "cannot get identity public key")
		}
	}
	svc, err := bakery.NewService(
		bakery.NewServiceParams{
			Location: "juju model " + st.ModelUUID(),
			Locator: bakery.PublicKeyLocatorMap{
				idURL: idPK,
			},
		},
	)
	if err != nil {
		return nil, errors.Annotate(err, "cannot make bakery service")
	}
	var auth authentication.MacaroonAuthenticator
	auth.Service = svc
	auth.Macaroon, err = svc.NewMacaroon("api-login", nil, nil)
	if err != nil {
		return nil, errors.Annotate(err, "cannot make macaroon")
	}
	auth.IdentityLocation = idURL
	return &auth, nil
}
Example #16
0
File: client.go Project: bac/juju
func newClient(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*Client, error) {
	urlGetter := common.NewToolsURLGetter(st.ModelUUID(), st)
	configGetter := stateenvirons.EnvironConfigGetter{st}
	statusSetter := common.NewStatusSetter(st, common.AuthAlways())
	toolsFinder := common.NewToolsFinder(configGetter, st, urlGetter)
	newEnviron := func() (environs.Environ, error) {
		return environs.GetEnviron(configGetter, environs.New)
	}
	blockChecker := common.NewBlockChecker(st)
	modelConfigAPI, err := modelconfig.NewModelConfigAPI(st, authorizer)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return NewClient(
		NewStateBackend(st),
		modelConfigAPI,
		resources,
		authorizer,
		statusSetter,
		toolsFinder,
		newEnviron,
		blockChecker,
	)
}
Example #17
0
// Run initializes state for an environment.
func (c *BootstrapCommand) Run(_ *cmd.Context) error {
	envCfg, err := config.New(config.NoDefaults, c.ControllerModelConfig)
	if err != nil {
		return err
	}
	err = c.ReadConfig("machine-0")
	if err != nil {
		return err
	}
	agentConfig := c.CurrentConfig()
	network.SetPreferIPv6(agentConfig.PreferIPv6())

	// agent.Jobs is an optional field in the agent config, and was
	// introduced after 1.17.2. We default to allowing units on
	// machine-0 if missing.
	jobs := agentConfig.Jobs()
	if len(jobs) == 0 {
		jobs = []multiwatcher.MachineJob{
			multiwatcher.JobManageModel,
			multiwatcher.JobHostUnits,
			multiwatcher.JobManageNetworking,
		}
	}

	// Get the bootstrap machine's addresses from the provider.
	env, err := environs.New(envCfg)
	if err != nil {
		return err
	}
	newConfigAttrs := make(map[string]interface{})

	// Check to see if a newer agent version has been requested
	// by the bootstrap client.
	desiredVersion, ok := envCfg.AgentVersion()
	if ok && desiredVersion != jujuversion.Current {
		// If we have been asked for a newer version, ensure the newer
		// tools can actually be found, or else bootstrap won't complete.
		stream := envtools.PreferredStream(&desiredVersion, envCfg.Development(), envCfg.AgentStream())
		logger.Infof("newer tools requested, looking for %v in stream %v", desiredVersion, stream)
		filter := tools.Filter{
			Number: desiredVersion,
			Arch:   arch.HostArch(),
			Series: series.HostSeries(),
		}
		_, toolsErr := envtools.FindTools(env, -1, -1, stream, filter)
		if toolsErr == nil {
			logger.Infof("tools are available, upgrade will occur after bootstrap")
		}
		if errors.IsNotFound(toolsErr) {
			// Newer tools not available, so revert to using the tools
			// matching the current agent version.
			logger.Warningf("newer tools for %q not available, sticking with version %q", desiredVersion, jujuversion.Current)
			newConfigAttrs["agent-version"] = jujuversion.Current.String()
		} else if toolsErr != nil {
			logger.Errorf("cannot find newer tools: %v", toolsErr)
			return toolsErr
		}
	}

	instanceId := instance.Id(c.InstanceId)
	instances, err := env.Instances([]instance.Id{instanceId})
	if err != nil {
		return err
	}
	addrs, err := instances[0].Addresses()
	if err != nil {
		return err
	}

	// When machine addresses are reported from state, they have
	// duplicates removed.  We should do the same here so that
	// there is not unnecessary churn in the mongo replicaset.
	// TODO (cherylj) Add explicit unit tests for this - tracked
	// by bug #1544158.
	addrs = network.MergedAddresses([]network.Address{}, addrs)

	// Generate a private SSH key for the controllers, and add
	// the public key to the environment config. We'll add the
	// private key to StateServingInfo below.
	privateKey, publicKey, err := sshGenerateKey(config.JujuSystemKey)
	if err != nil {
		return errors.Annotate(err, "failed to generate system key")
	}
	authorizedKeys := config.ConcatAuthKeys(envCfg.AuthorizedKeys(), publicKey)
	newConfigAttrs[config.AuthKeysConfig] = authorizedKeys

	// Generate a shared secret for the Mongo replica set, and write it out.
	sharedSecret, err := mongo.GenerateSharedSecret()
	if err != nil {
		return err
	}
	info, ok := agentConfig.StateServingInfo()
	if !ok {
		return fmt.Errorf("bootstrap machine config has no state serving info")
	}
	info.SharedSecret = sharedSecret
	info.SystemIdentity = privateKey
	err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error {
		agentConfig.SetStateServingInfo(info)
		return nil
	})
	if err != nil {
		return fmt.Errorf("cannot write agent config: %v", err)
	}

	err = c.ChangeConfig(func(config agent.ConfigSetter) error {
		// We'll try for mongo 3.2 first and fallback to
		// mongo 2.4 if the newer binaries are not available.
		if mongo.BinariesAvailable(mongo.Mongo32wt) {
			config.SetMongoVersion(mongo.Mongo32wt)
		} else {
			config.SetMongoVersion(mongo.Mongo24)
		}
		return nil
	})
	if err != nil {
		return errors.Annotate(err, "cannot set mongo version")
	}

	agentConfig = c.CurrentConfig()

	// Create system-identity file
	if err := agent.WriteSystemIdentityFile(agentConfig); err != nil {
		return err
	}

	if err := c.startMongo(addrs, agentConfig); err != nil {
		return err
	}

	logger.Infof("started mongo")
	// Initialise state, and store any agent config (e.g. password) changes.
	envCfg, err = env.Config().Apply(newConfigAttrs)
	if err != nil {
		return errors.Annotate(err, "failed to update model config")
	}
	var st *state.State
	var m *state.Machine
	err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error {
		var stateErr error
		dialOpts := mongo.DefaultDialOpts()

		// Set a longer socket timeout than usual, as the machine
		// will be starting up and disk I/O slower than usual. This
		// has been known to cause timeouts in queries.
		timeouts := envCfg.BootstrapSSHOpts()
		dialOpts.SocketTimeout = timeouts.Timeout
		if dialOpts.SocketTimeout < minSocketTimeout {
			dialOpts.SocketTimeout = minSocketTimeout
		}

		// We shouldn't attempt to dial peers until we have some.
		dialOpts.Direct = true

		adminTag := names.NewLocalUserTag(c.AdminUsername)
		st, m, stateErr = agentInitializeState(
			adminTag,
			agentConfig,
			envCfg, c.HostedModelConfig,
			agentbootstrap.BootstrapMachineConfig{
				Addresses:            addrs,
				BootstrapConstraints: c.BootstrapConstraints,
				ModelConstraints:     c.ModelConstraints,
				Jobs:                 jobs,
				InstanceId:           instanceId,
				Characteristics:      c.Hardware,
				SharedSecret:         sharedSecret,
			},
			dialOpts,
			environs.NewStatePolicy(),
		)
		return stateErr
	})
	if err != nil {
		return err
	}
	defer st.Close()

	// Populate the tools catalogue.
	if err := c.populateTools(st, env); err != nil {
		return err
	}

	// Populate the GUI archive catalogue.
	if err := c.populateGUIArchive(st, env); err != nil {
		// Do not stop the bootstrapping process for Juju GUI archive errors.
		logger.Warningf("cannot set up Juju GUI: %s", err)
	} else {
		logger.Debugf("Juju GUI successfully set up")
	}

	// Add custom image metadata to environment storage.
	if c.ImageMetadataDir != "" {
		if err := c.saveCustomImageMetadata(st, env); err != nil {
			return err
		}

		stor := newStateStorage(st.ModelUUID(), st.MongoSession())
		if err := c.storeCustomImageMetadata(stor); err != nil {
			return err
		}
	}

	// Populate the storage pools.
	if err = c.populateDefaultStoragePools(st); err != nil {
		return err
	}

	// bootstrap machine always gets the vote
	return m.SetHasVote(true)
}
Example #18
0
func isMigrationActive(c *gc.C, st *state.State) bool {
	isActive, err := state.IsModelMigrationActive(st, st.ModelUUID())
	c.Assert(err, jc.ErrorIsNil)
	return isActive
}
Example #19
0
func (s *migrationSuite) startSync(c *gc.C, st *state.State) {
	backingSt, err := s.BackingStatePool.Get(st.ModelUUID())
	c.Assert(err, jc.ErrorIsNil)
	backingSt.StartSync()
}
Example #20
0
// newBakeryService creates a new bakery.Service.
func newBakeryService(st *state.State, locator bakery.PublicKeyLocator) (*bakery.Service, error) {
	return bakery.NewService(bakery.NewServiceParams{
		Location: "juju model " + st.ModelUUID(),
		Locator:  locator,
	})
}
Example #21
0
func (s *LogsSuite) countLogs(c *gc.C, st *state.State) int {
	count, err := s.logsColl.Find(bson.M{"e": st.ModelUUID()}).Count()
	c.Assert(err, jc.ErrorIsNil)
	return count
}