github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/migration/migration.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package migration 5 6 import ( 7 "io" 8 "io/ioutil" 9 "os" 10 11 "github.com/juju/errors" 12 "github.com/juju/loggo" 13 "github.com/juju/utils/set" 14 "github.com/juju/version" 15 "gopkg.in/juju/charm.v6-unstable" 16 "gopkg.in/mgo.v2" 17 18 "github.com/juju/juju/api" 19 "github.com/juju/juju/core/description" 20 "github.com/juju/juju/environs" 21 "github.com/juju/juju/environs/config" 22 "github.com/juju/juju/state" 23 "github.com/juju/juju/state/binarystorage" 24 "github.com/juju/juju/state/storage" 25 "github.com/juju/juju/tools" 26 ) 27 28 var logger = loggo.GetLogger("juju.migration") 29 30 // StateExporter describes interface on state required to export a 31 // model. 32 type StateExporter interface { 33 // Export generates an abstract representation of a model. 34 Export() (description.Model, error) 35 } 36 37 // ExportModel creates a description.Model representation of the 38 // active model for StateExporter (typically a *state.State), and 39 // returns the serialized version. It provides the symmetric 40 // functionality to ImportModel. 41 func ExportModel(st StateExporter) ([]byte, error) { 42 model, err := st.Export() 43 if err != nil { 44 return nil, errors.Trace(err) 45 } 46 bytes, err := description.Serialize(model) 47 if err != nil { 48 return nil, errors.Trace(err) 49 } 50 return bytes, nil 51 } 52 53 // ImportModel deserializes a model description from the bytes, transforms 54 // the model config based on information from the controller model, and then 55 // imports that as a new database model. 56 func ImportModel(st *state.State, bytes []byte) (*state.Model, *state.State, error) { 57 model, err := description.Deserialize(bytes) 58 if err != nil { 59 return nil, nil, errors.Trace(err) 60 } 61 62 controllerModel, err := st.ControllerModel() 63 if err != nil { 64 return nil, nil, errors.Trace(err) 65 } 66 67 controllerConfig, err := controllerModel.Config() 68 if err != nil { 69 return nil, nil, errors.Trace(err) 70 } 71 72 model.UpdateConfig(controllerValues(controllerConfig)) 73 74 if err := updateConfigFromProvider(model, controllerConfig); err != nil { 75 return nil, nil, errors.Trace(err) 76 } 77 78 dbModel, dbState, err := st.Import(model) 79 if err != nil { 80 return nil, nil, errors.Trace(err) 81 } 82 return dbModel, dbState, nil 83 } 84 85 func controllerValues(config *config.Config) map[string]interface{} { 86 result := make(map[string]interface{}) 87 88 result["state-port"] = config.StatePort() 89 result["api-port"] = config.APIPort() 90 result["controller-uuid"] = config.ControllerUUID() 91 // We ignore the second bool param from the CACert check as if there 92 // wasn't a CACert, there is no way we'd be importing a new model 93 // into the controller 94 result["ca-cert"], _ = config.CACert() 95 return result 96 } 97 98 func updateConfigFromProvider(model description.Model, controllerConfig *config.Config) error { 99 newConfig, err := config.New(config.NoDefaults, model.Config()) 100 if err != nil { 101 return errors.Trace(err) 102 } 103 104 provider, err := environs.New(newConfig) 105 if err != nil { 106 return errors.Trace(err) 107 } 108 109 updater, ok := provider.(environs.MigrationConfigUpdater) 110 if !ok { 111 return nil 112 } 113 114 model.UpdateConfig(updater.MigrationConfigUpdate(controllerConfig)) 115 return nil 116 } 117 118 // UploadBackend define the methods on *state.State that are needed for 119 // uploading the tools and charms from the current controller to a different 120 // controller. 121 type UploadBackend interface { 122 Charm(*charm.URL) (*state.Charm, error) 123 ModelUUID() string 124 MongoSession() *mgo.Session 125 ToolsStorage() (binarystorage.StorageCloser, error) 126 } 127 128 // CharmUploader defines a simple single method interface that is used to 129 // upload a charm to the target controller 130 type CharmUploader interface { 131 UploadCharm(*charm.URL, io.ReadSeeker) (*charm.URL, error) 132 } 133 134 // ToolsUploader defines a simple single method interface that is used to 135 // upload tools to the target controller 136 type ToolsUploader interface { 137 UploadTools(io.ReadSeeker, version.Binary, ...string) (tools.List, error) 138 } 139 140 // UploadBinariesConfig provides all the configuration that the UploadBinaries 141 // function needs to operate. The functions are configurable for testing 142 // purposes. To construct the config with the default functions, use 143 // `NewUploadBinariesConfig`. 144 type UploadBinariesConfig struct { 145 State UploadBackend 146 Model description.Model 147 Target api.Connection 148 149 GetCharmUploader func(api.Connection) CharmUploader 150 GetToolsUploader func(api.Connection) ToolsUploader 151 152 GetStateStorage func(UploadBackend) storage.Storage 153 GetCharmStoragePath func(UploadBackend, *charm.URL) (string, error) 154 } 155 156 // NewUploadBinariesConfig constructs a `UploadBinariesConfig` with the default 157 // functions to get the uploaders for the target api connection, and functions 158 // used to get the charm data out of the database. 159 func NewUploadBinariesConfig(backend UploadBackend, model description.Model, target api.Connection) UploadBinariesConfig { 160 return UploadBinariesConfig{ 161 State: backend, 162 Model: model, 163 Target: target, 164 165 GetCharmUploader: getCharmUploader, 166 GetStateStorage: getStateStorage, 167 GetToolsUploader: getToolsUploader, 168 GetCharmStoragePath: getCharmStoragePath, 169 } 170 } 171 172 // Validate makes sure that all the config values are non-nil. 173 func (c *UploadBinariesConfig) Validate() error { 174 if c.State == nil { 175 return errors.NotValidf("missing UploadBackend") 176 } 177 if c.Model == nil { 178 return errors.NotValidf("missing Model") 179 } 180 if c.Target == nil { 181 return errors.NotValidf("missing Target") 182 } 183 if c.GetCharmUploader == nil { 184 return errors.NotValidf("missing GetCharmUploader") 185 } 186 if c.GetStateStorage == nil { 187 return errors.NotValidf("missing GetStateStorage") 188 } 189 if c.GetToolsUploader == nil { 190 return errors.NotValidf("missing GetToolsUploader") 191 } 192 if c.GetCharmStoragePath == nil { 193 return errors.NotValidf("missing GetCharmStoragePath") 194 } 195 return nil 196 } 197 198 // UploadBinaries will send binaries stored in the source blobstore to 199 // the target controller. 200 func UploadBinaries(config UploadBinariesConfig) error { 201 if err := config.Validate(); err != nil { 202 return errors.Trace(err) 203 } 204 if err := uploadTools(config); err != nil { 205 return errors.Trace(err) 206 } 207 208 if err := uploadCharms(config); err != nil { 209 return errors.Trace(err) 210 } 211 212 return nil 213 } 214 215 func getStateStorage(backend UploadBackend) storage.Storage { 216 return storage.NewStorage(backend.ModelUUID(), backend.MongoSession()) 217 } 218 219 func getToolsUploader(target api.Connection) ToolsUploader { 220 return target.Client() 221 } 222 223 func getCharmUploader(target api.Connection) CharmUploader { 224 return target.Client() 225 } 226 227 func uploadTools(config UploadBinariesConfig) error { 228 storage, err := config.State.ToolsStorage() 229 if err != nil { 230 return errors.Trace(err) 231 } 232 defer storage.Close() 233 234 usedVersions := getUsedToolsVersions(config.Model) 235 toolsUploader := config.GetToolsUploader(config.Target) 236 237 for toolsVersion := range usedVersions { 238 logger.Debugf("send tools version %s to target", toolsVersion) 239 _, reader, err := storage.Open(toolsVersion.String()) 240 if err != nil { 241 return errors.Trace(err) 242 } 243 defer reader.Close() 244 245 content, cleanup, err := streamThroughTempFile(reader) 246 if err != nil { 247 return errors.Trace(err) 248 } 249 defer cleanup() 250 251 // UploadTools encapsulates the HTTP POST necessary to send the tools 252 // to the target API server. 253 if _, err := toolsUploader.UploadTools(content, toolsVersion); err != nil { 254 return errors.Trace(err) 255 } 256 } 257 258 return nil 259 } 260 261 func streamThroughTempFile(r io.Reader) (_ io.ReadSeeker, cleanup func(), err error) { 262 tempFile, err := ioutil.TempFile("", "juju-tools") 263 if err != nil { 264 return nil, nil, errors.Trace(err) 265 } 266 defer func() { 267 if err != nil { 268 os.Remove(tempFile.Name()) 269 } 270 }() 271 _, err = io.Copy(tempFile, r) 272 if err != nil { 273 return nil, nil, errors.Trace(err) 274 } 275 tempFile.Seek(0, 0) 276 rmTempFile := func() { 277 filename := tempFile.Name() 278 tempFile.Close() 279 os.Remove(filename) 280 } 281 282 return tempFile, rmTempFile, nil 283 } 284 285 func getUsedToolsVersions(model description.Model) map[version.Binary]bool { 286 // Iterate through the model for all tools, and make a map of them. 287 usedVersions := make(map[version.Binary]bool) 288 // It is most likely that the preconditions will limit the number of 289 // tools versions in use, but that is not depended on here. 290 for _, machine := range model.Machines() { 291 addToolsVersionForMachine(machine, usedVersions) 292 } 293 294 for _, service := range model.Services() { 295 for _, unit := range service.Units() { 296 tools := unit.Tools() 297 usedVersions[tools.Version()] = true 298 } 299 } 300 return usedVersions 301 } 302 303 func addToolsVersionForMachine(machine description.Machine, usedVersions map[version.Binary]bool) { 304 tools := machine.Tools() 305 usedVersions[tools.Version()] = true 306 for _, container := range machine.Containers() { 307 addToolsVersionForMachine(container, usedVersions) 308 } 309 } 310 311 func uploadCharms(config UploadBinariesConfig) error { 312 storage := config.GetStateStorage(config.State) 313 usedCharms := getUsedCharms(config.Model) 314 charmUploader := config.GetCharmUploader(config.Target) 315 316 for _, charmUrl := range usedCharms.Values() { 317 logger.Debugf("send charm %s to target", charmUrl) 318 319 curl, err := charm.ParseURL(charmUrl) 320 if err != nil { 321 return errors.Annotate(err, "bad charm URL") 322 } 323 324 path, err := config.GetCharmStoragePath(config.State, curl) 325 if err != nil { 326 return errors.Trace(err) 327 } 328 329 reader, _, err := storage.Get(path) 330 if err != nil { 331 return errors.Annotate(err, "cannot get charm from storage") 332 } 333 defer reader.Close() 334 335 content, cleanup, err := streamThroughTempFile(reader) 336 if err != nil { 337 return errors.Trace(err) 338 } 339 defer cleanup() 340 341 if _, err := charmUploader.UploadCharm(curl, content); err != nil { 342 return errors.Annotate(err, "cannot upload charm") 343 } 344 } 345 return nil 346 } 347 348 func getUsedCharms(model description.Model) set.Strings { 349 result := set.NewStrings() 350 for _, service := range model.Services() { 351 result.Add(service.CharmURL()) 352 } 353 return result 354 } 355 356 func getCharmStoragePath(st UploadBackend, curl *charm.URL) (string, error) { 357 ch, err := st.Charm(curl) 358 if err != nil { 359 return "", errors.Annotate(err, "cannot get charm from state") 360 } 361 362 return ch.StoragePath(), nil 363 } 364 365 // PrecheckBackend is implemented by *state.State but defined as an interface 366 // for easier testing. 367 type PrecheckBackend interface { 368 NeedsCleanup() (bool, error) 369 } 370 371 // Precheck checks the database state to make sure that the preconditions 372 // for model migration are met. 373 func Precheck(backend PrecheckBackend) error { 374 cleanupNeeded, err := backend.NeedsCleanup() 375 if err != nil { 376 return errors.Annotate(err, "precheck cleanups") 377 } 378 if cleanupNeeded { 379 return errors.New("precheck failed: cleanup needed") 380 } 381 return nil 382 }