func (s *serviceSuite) TestAddCharmConcurrently(c *gc.C) { var putBarrier sync.WaitGroup var blobs blobs s.PatchValue(service.NewStateStorage, func(uuid string, session *mgo.Session) statestorage.Storage { storage := statestorage.NewStorage(uuid, session) return &recordingStorage{Storage: storage, blobs: &blobs, putBarrier: &putBarrier} }) client := s.APIState.Client() curl, _ := s.UploadCharm(c, "trusty/wordpress-3", "wordpress") // Try adding the same charm concurrently from multiple goroutines // to test no "duplicate key errors" are reported (see lp bug // #1067979) and also at the end only one charm document is // created. var wg sync.WaitGroup // We don't add them 1-by-1 because that would allow each goroutine to // finish separately without actually synchronizing between them putBarrier.Add(10) for i := 0; i < 10; i++ { wg.Add(1) go func(index int) { defer wg.Done() c.Assert(client.AddCharm(curl), gc.IsNil, gc.Commentf("goroutine %d", index)) sch, err := s.State.Charm(curl) c.Assert(err, gc.IsNil, gc.Commentf("goroutine %d", index)) c.Assert(sch.URL(), jc.DeepEquals, curl, gc.Commentf("goroutine %d", index)) }(i) } wg.Wait() blobs.Lock() c.Assert(blobs.m, gc.HasLen, 10) // Verify there is only a single uploaded charm remains and it // contains the correct data. sch, err := s.State.Charm(curl) c.Assert(err, jc.ErrorIsNil) storagePath := sch.StoragePath() c.Assert(blobs.m[storagePath], jc.IsTrue) for path, exists := range blobs.m { if path != storagePath { c.Assert(exists, jc.IsFalse) } } storage := statestorage.NewStorage(s.State.EnvironUUID(), s.State.MongoSession()) s.assertUploaded(c, storage, sch.StoragePath(), sch.BundleSha256()) }
// 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 }
// Remove will delete the charm's stored archive and render the charm // inaccessible to future clients. It will fail unless the charm is // already Dying (indicating that someone has called Destroy). func (c *Charm) Remove() error { switch c.doc.Life { case Alive: return errors.New("still alive") case Dead: return nil } stor := storage.NewStorage(c.st.ModelUUID(), c.st.MongoSession()) err := stor.Remove(c.doc.StoragePath) if errors.IsNotFound(err) { // Not a problem, but we might still need to run the // transaction further down to complete the process. } else if err != nil { return errors.Annotate(err, "deleting archive") } buildTxn := func(_ int) ([]txn.Op, error) { ops, err := charmRemoveOps(c.st, c.doc.URL) switch errors.Cause(err) { case nil: case errAlreadyDead: return nil, jujutxn.ErrNoOperations default: return nil, errors.Trace(err) } return ops, nil } if err := c.st.run(buildTxn); err != nil { return errors.Trace(err) } c.doc.Life = Dead return nil }
func (s *migrateCharmStorageSuite) testMigrateCharmStorage(c *gc.C, curl *charm.URL, agentConfig agent.Config) { curlPlaceholder := charm.MustParseURL("cs:quantal/dummy-1") err := s.State.AddStoreCharmPlaceholder(curlPlaceholder) c.Assert(err, jc.ErrorIsNil) curlPending := charm.MustParseURL("cs:quantal/missing-123") _, err = s.State.PrepareStoreCharmUpload(curlPending) c.Assert(err, jc.ErrorIsNil) var storagePath string var called bool s.PatchValue(upgrades.StateAddCharmStoragePaths, func(st *state.State, storagePaths map[*charm.URL]string) error { c.Assert(storagePaths, gc.HasLen, 1) for k, v := range storagePaths { c.Assert(k.String(), gc.Equals, curl.String()) storagePath = v } called = true return nil }) err = upgrades.MigrateCharmStorage(s.State, agentConfig) c.Assert(err, jc.ErrorIsNil) c.Assert(called, jc.IsTrue) storage := storage.NewStorage(s.State.EnvironUUID(), s.State.MongoSession()) r, length, err := storage.Get(storagePath) c.Assert(err, jc.ErrorIsNil) c.Assert(r, gc.NotNil) defer r.Close() c.Assert(length, gc.Equals, int64(3)) data, err := ioutil.ReadAll(r) c.Assert(err, jc.ErrorIsNil) c.Assert(string(data), gc.Equals, "abc") }
// NewStorage returns a new blob storage for the environment. func (sp *statePersistence) NewStorage() storage.Storage { envUUID := sp.st.ModelUUID() // TODO(ericsnow) Copy the session? session := sp.st.session store := storage.NewStorage(envUUID, session) return store }
func (s serveCharm) step(c *gc.C, ctx *context) { storage := storage.NewStorage(ctx.st.ModelUUID(), ctx.st.MongoSession()) for storagePath, data := range ctx.charms { err := storage.Put(storagePath, bytes.NewReader(data), int64(len(data))) c.Assert(err, jc.ErrorIsNil) delete(ctx.charms, storagePath) } }
// migrateCharmStorage copies uploaded charms from provider storage // to environment storage, and then adds the storage path into the // charm's document in state. func migrateCharmStorage(st *state.State, agentConfig agent.Config) error { logger.Debugf("migrating charms to environment storage") charms, err := st.AllCharms() if err != nil { return err } storage := storage.NewStorage(st.EnvironUUID(), st.MongoSession()) // Local and manual provider host storage on the state server's // filesystem, and serve via HTTP storage. The storage worker // doesn't run yet, so we just open the files directly. fetchCharmArchive := fetchCharmArchive providerType := agentConfig.Value(agent.ProviderType) if providerType == provider.Local || provider.IsManual(providerType) { storageDir := agentConfig.Value(agent.StorageDir) fetchCharmArchive = localstorage{storageDir}.fetchCharmArchive } storagePaths := make(map[*charm.URL]string) for _, ch := range charms { if ch.IsPlaceholder() { logger.Debugf("skipping %s, placeholder charm", ch.URL()) continue } if !ch.IsUploaded() { logger.Debugf("skipping %s, not uploaded to provider storage", ch.URL()) continue } if charmStoragePath(ch) != "" { logger.Debugf("skipping %s, already in environment storage", ch.URL()) continue } url := charmBundleURL(ch) if url == nil { logger.Debugf("skipping %s, has no bundle URL", ch.URL()) continue } uuid, err := utils.NewUUID() if err != nil { return err } data, err := fetchCharmArchive(url) if err != nil { return err } curl := ch.URL() storagePath := fmt.Sprintf("charms/%s-%s", curl, uuid) logger.Debugf("uploading %s to %q in environment storage", curl, storagePath) err = storage.Put(storagePath, bytes.NewReader(data), int64(len(data))) if err != nil { return errors.Annotatef(err, "failed to upload %s to storage", curl) } storagePaths[curl] = storagePath } return stateAddCharmStoragePaths(st, storagePaths) }
func (s *StorageSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) s.MgoSuite.SetUpTest(c) rs := blobstore.NewGridFS("blobstore", testUUID, s.Session) db := s.Session.DB("juju") s.managedStorage = blobstore.NewManagedStorage(db, rs) s.storage = storage.NewStorage(testUUID, s.Session) }
func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) { // Make a clone of the dummy charm in a nested directory. rootDir := c.MkDir() dirPath := filepath.Join(rootDir, "subdir1", "subdir2") err := os.MkdirAll(dirPath, 0755) c.Assert(err, jc.ErrorIsNil) dir := testcharms.Repo.ClonedDir(dirPath, "dummy") // Now tweak the path the dir thinks it is in and bundle it. dir.Path = rootDir tempFile, err := ioutil.TempFile(c.MkDir(), "charm") c.Assert(err, jc.ErrorIsNil) defer tempFile.Close() defer os.Remove(tempFile.Name()) err = dir.ArchiveTo(tempFile) c.Assert(err, jc.ErrorIsNil) // Try reading it as a bundle - should fail due to nested dirs. _, err = charm.ReadCharmArchive(tempFile.Name()) c.Assert(err, gc.ErrorMatches, `archive file "metadata.yaml" not found`) // Now try uploading it - should succeeed and be repackaged. resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name()) c.Assert(err, jc.ErrorIsNil) expectedURL := charm.MustParseURL("local:quantal/dummy-1") s.assertUploadResponse(c, resp, expectedURL.String()) sch, err := s.State.Charm(expectedURL) c.Assert(err, jc.ErrorIsNil) c.Assert(sch.URL(), gc.DeepEquals, expectedURL) c.Assert(sch.Revision(), gc.Equals, 1) c.Assert(sch.IsUploaded(), jc.IsTrue) // Get it from the storage and try to read it as a bundle - it // should succeed, because it was repackaged during upload to // strip nested dirs. storage := storage.NewStorage(s.State.EnvironUUID(), s.State.MongoSession()) reader, _, err := storage.Get(sch.StoragePath()) c.Assert(err, jc.ErrorIsNil) defer reader.Close() data, err := ioutil.ReadAll(reader) c.Assert(err, jc.ErrorIsNil) downloadedFile, err := ioutil.TempFile(c.MkDir(), "downloaded") c.Assert(err, jc.ErrorIsNil) defer downloadedFile.Close() defer os.Remove(downloadedFile.Name()) err = ioutil.WriteFile(downloadedFile.Name(), data, 0644) c.Assert(err, jc.ErrorIsNil) bundle, err := charm.ReadCharmArchive(downloadedFile.Name()) c.Assert(err, jc.ErrorIsNil) c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision()) c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta()) c.Assert(bundle.Config(), jc.DeepEquals, sch.Config()) }
func (s *serviceSuite) TestAddCharm(c *gc.C) { var blobs blobs s.PatchValue(service.NewStateStorage, func(uuid string, session *mgo.Session) statestorage.Storage { storage := statestorage.NewStorage(uuid, session) return &recordingStorage{Storage: storage, blobs: &blobs} }) client := s.APIState.Client() // First test the sanity checks. err := client.AddCharm(&charm.URL{Name: "nonsense"}) c.Assert(err, gc.ErrorMatches, `charm URL has invalid schema: ":nonsense-0"`) err = client.AddCharm(charm.MustParseURL("local:precise/dummy")) c.Assert(err, gc.ErrorMatches, "only charm store charm URLs are supported, with cs: schema") err = client.AddCharm(charm.MustParseURL("cs:precise/wordpress")) c.Assert(err, gc.ErrorMatches, "charm URL must include revision") // Add a charm, without uploading it to storage, to // check that AddCharm does not try to do it. charmDir := testcharms.Repo.CharmDir("dummy") ident := fmt.Sprintf("%s-%d", charmDir.Meta().Name, charmDir.Revision()) curl := charm.MustParseURL("cs:quantal/" + ident) sch, err := s.State.AddCharm(charmDir, curl, "", ident+"-sha256") c.Assert(err, jc.ErrorIsNil) // AddCharm should see the charm in state and not upload it. err = client.AddCharm(sch.URL()) c.Assert(err, jc.ErrorIsNil) c.Assert(blobs.m, gc.HasLen, 0) // Now try adding another charm completely. curl, _ = s.UploadCharm(c, "precise/wordpress-3", "wordpress") err = client.AddCharm(curl) c.Assert(err, jc.ErrorIsNil) // Verify it's in state and it got uploaded. storage := statestorage.NewStorage(s.State.EnvironUUID(), s.State.MongoSession()) sch, err = s.State.Charm(curl) c.Assert(err, jc.ErrorIsNil) s.assertUploaded(c, storage, sch.StoragePath(), sch.BundleSha256()) }
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 }
func (s *RepoSuite) AssertCharmUploaded(c *gc.C, curl *charm.URL) { ch, err := s.State.Charm(curl) c.Assert(err, jc.ErrorIsNil) storage := storage.NewStorage(s.State.EnvironUUID(), s.State.MongoSession()) r, _, err := storage.Get(ch.StoragePath()) c.Assert(err, jc.ErrorIsNil) defer r.Close() digest, _, err := utils.ReadSHA256(r) c.Assert(err, jc.ErrorIsNil) c.Assert(ch.BundleSha256(), gc.Equals, digest) }
func (s *CharmSuite) TestRemoveDeletesStorage(c *gc.C) { // We normally don't actually set up charm storage in state // tests, but we need it here. path := s.charm.StoragePath() stor := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession()) err := stor.Put(path, strings.NewReader("abc"), 3) c.Assert(err, jc.ErrorIsNil) s.destroy(c) closer, _, err := stor.Get(path) c.Assert(err, jc.ErrorIsNil) closer.Close() s.remove(c) _, _, err = stor.Get(path) c.Assert(err, jc.Satisfies, errors.IsNotFound) }
func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) { // Make a dummy charm dir with revision 123. dir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy") dir.SetDiskRevision(123) // Now bundle the dir. tempFile, err := ioutil.TempFile(c.MkDir(), "charm") c.Assert(err, jc.ErrorIsNil) defer tempFile.Close() defer os.Remove(tempFile.Name()) err = dir.ArchiveTo(tempFile) c.Assert(err, jc.ErrorIsNil) // Now try uploading it and ensure the revision persists. resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name()) c.Assert(err, jc.ErrorIsNil) expectedURL := charm.MustParseURL("local:quantal/dummy-123") s.assertUploadResponse(c, resp, expectedURL.String()) sch, err := s.State.Charm(expectedURL) c.Assert(err, jc.ErrorIsNil) c.Assert(sch.URL(), gc.DeepEquals, expectedURL) c.Assert(sch.Revision(), gc.Equals, 123) c.Assert(sch.IsUploaded(), jc.IsTrue) // First rewind the reader, which was reset but BundleTo() above. _, err = tempFile.Seek(0, 0) c.Assert(err, jc.ErrorIsNil) // Finally, verify the SHA256. expectedSHA256, _, err := utils.ReadSHA256(tempFile) c.Assert(err, jc.ErrorIsNil) c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256) storage := storage.NewStorage(s.State.EnvironUUID(), s.State.MongoSession()) reader, _, err := storage.Get(sch.StoragePath()) c.Assert(err, jc.ErrorIsNil) defer reader.Close() downloadedSHA256, _, err := utils.ReadSHA256(reader) c.Assert(err, jc.ErrorIsNil) c.Assert(downloadedSHA256, gc.Equals, expectedSHA256) }
func (s *CleanupSuite) TestCleanupDyingServiceCharm(c *gc.C) { // Create a service and a charm. ch := s.AddTestingCharm(c, "mysql") mysql := s.AddTestingService(c, "mysql", ch) // Create a dummy archive blob. stor := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession()) storagePath := "dummy-path" err := stor.Put(storagePath, bytes.NewReader([]byte("data")), 4) c.Assert(err, jc.ErrorIsNil) // Destroy the service and check that a cleanup has been scheduled. err = mysql.Destroy() c.Assert(err, jc.ErrorIsNil) s.assertNeedsCleanup(c) // Run the cleanup, and check that the charm is removed. s.assertCleanupRuns(c) _, _, err = stor.Get(storagePath) c.Assert(err, jc.Satisfies, errors.IsNotFound) }
// 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 }
func getStateStorage(backend UploadBackend) storage.Storage { return storage.NewStorage(backend.ModelUUID(), backend.MongoSession()) }
func (c *upgradeWorkerContext) run(stop <-chan struct{}) error { if wrench.IsActive("machine-agent", "fail-upgrade-start") { return nil // Make the worker stop } select { case <-c.UpgradeComplete: // Our work is already done (we're probably being restarted // because the API connection has gone down), so do nothing. return nil default: } c.agentConfig = c.agent.CurrentConfig() c.fromVersion = c.agentConfig.UpgradedToVersion() c.toVersion = version.Current.Number if c.fromVersion == c.toVersion { logger.Infof("upgrade to %v already completed.", c.toVersion) close(c.UpgradeComplete) return nil } if err := c.initTag(c.agentConfig.Tag()); err != nil { return errors.Trace(err) } // If the machine agent is a state server, flag that state // needs to be opened before running upgrade steps for _, job := range c.jobs { if job == multiwatcher.JobManageEnviron { c.isStateServer = true } } // We need a *state.State for upgrades. We open it independently // of StateWorker, because we have no guarantees about when // and how often StateWorker might run. if c.isStateServer { var err error if c.st, err = openStateForUpgrade(c.agent, c.agentConfig); err != nil { return err } defer c.st.Close() if c.isMaster, err = isMachineMaster(c.st, c.machineId); err != nil { return errors.Trace(err) } stor := storage.NewStorage(c.st.EnvironUUID(), c.st.MongoSession()) registerSimplestreamsDataSource(stor) } if err := c.runUpgrades(); err != nil { // Only return an error from the worker if the connection to // state went away (possible mongo master change). Returning // an error when the connection is lost will cause the agent // to restart. // // For other errors, the error is not returned because we want // the machine agent to stay running in an error state waiting // for user intervention. if isAPILostDuringUpgrade(err) { return err } c.reportUpgradeFailure(err, false) } else { // Upgrade succeeded - signal that the upgrade is complete. logger.Infof("upgrade to %v completed successfully.", c.toVersion) c.agent.setMachineStatus(c.apiState, params.StatusStarted, "") close(c.UpgradeComplete) } return nil }