github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "net/url" 10 "os" 11 12 "github.com/juju/description" 13 "github.com/juju/errors" 14 "github.com/juju/loggo" 15 "github.com/juju/naturalsort" 16 "github.com/juju/version" 17 "gopkg.in/juju/charm.v6" 18 19 "github.com/juju/juju/core/leadership" 20 "github.com/juju/juju/core/migration" 21 "github.com/juju/juju/feature" 22 "github.com/juju/juju/resource" 23 "github.com/juju/juju/state" 24 "github.com/juju/juju/tools" 25 ) 26 27 var logger = loggo.GetLogger("juju.migration") 28 29 // StateExporter describes interface on state required to export a 30 // model. 31 type StateExporter interface { 32 // Export generates an abstract representation of a model. 33 Export() (description.Model, error) 34 } 35 36 // ExportModel creates a description.Model representation of the 37 // active model for StateExporter (typically a *state.State), and 38 // returns the serialized version. It provides the symmetric 39 // functionality to ImportModel. 40 func ExportModel(st StateExporter) ([]byte, error) { 41 model, err := st.Export() 42 if err != nil { 43 return nil, errors.Trace(err) 44 } 45 bytes, err := description.Serialize(model) 46 if err != nil { 47 return nil, errors.Trace(err) 48 } 49 return bytes, nil 50 } 51 52 // StateImporter describes the method needed to import a model 53 // into the database. 54 type StateImporter interface { 55 Import(model description.Model) (*state.Model, *state.State, error) 56 } 57 58 // ClaimerFunc is a function that returns a leadership claimer for the 59 // model UUID passed. 60 type ClaimerFunc func(string) (leadership.Claimer, error) 61 62 // ImportModel deserializes a model description from the bytes, transforms 63 // the model config based on information from the controller model, and then 64 // imports that as a new database model. 65 func ImportModel(importer StateImporter, getClaimer ClaimerFunc, bytes []byte) (*state.Model, *state.State, error) { 66 model, err := description.Deserialize(bytes) 67 if err != nil { 68 return nil, nil, errors.Trace(err) 69 } 70 71 dbModel, dbState, err := importer.Import(model) 72 if err != nil { 73 return nil, nil, errors.Trace(err) 74 } 75 76 config, err := dbState.ControllerConfig() 77 if err != nil { 78 return nil, nil, errors.Trace(err) 79 } 80 81 // If we're using legacy-leases we get the claimer from the new 82 // state - otherwise use the function passed in. 83 // 84 var claimer leadership.Claimer 85 if config.Features().Contains(feature.LegacyLeases) { 86 claimer = dbState.LeadershipClaimer() 87 } else { 88 claimer, err = getClaimer(dbModel.UUID()) 89 if err != nil { 90 return nil, nil, errors.Annotate(err, "getting leadership claimer") 91 } 92 } 93 94 logger.Debugf("importing leadership") 95 for _, application := range model.Applications() { 96 if application.Leader() == "" { 97 continue 98 } 99 // When we import a new model, we need to give the leaders 100 // some time to settle. We don't want to have leader switches 101 // just because we migrated a model, so this time needs to be 102 // long enough to make sure we cover the time taken to migrate 103 // a reasonable sized model. We don't yet know how long this 104 // is going to be, but we need something. 105 // TODO(babbageclunk): Handle this better - maybe a way to 106 // suppress leadership expiries for a model until it's 107 // finished importing? 108 logger.Debugf("%q is the leader for %q", application.Leader(), application.Name()) 109 err := claimer.ClaimLeadership( 110 application.Name(), 111 application.Leader(), 112 state.InitialLeaderClaimTime, 113 ) 114 if err != nil { 115 return nil, nil, errors.Annotatef( 116 err, 117 "claiming leadership for %q", 118 application.Leader(), 119 ) 120 } 121 } 122 123 return dbModel, dbState, nil 124 } 125 126 // CharmDownlaoder defines a single method that is used to download a 127 // charm from the source controller in a migration. 128 type CharmDownloader interface { 129 OpenCharm(*charm.URL) (io.ReadCloser, error) 130 } 131 132 // CharmUploader defines a single method that is used to upload a 133 // charm to the target controller in a migration. 134 type CharmUploader interface { 135 UploadCharm(*charm.URL, io.ReadSeeker) (*charm.URL, error) 136 } 137 138 // ToolsDownloader defines a single method that is used to download 139 // tools from the source controller in a migration. 140 type ToolsDownloader interface { 141 OpenURI(string, url.Values) (io.ReadCloser, error) 142 } 143 144 // ToolsUploader defines a single method that is used to upload tools 145 // to the target controller in a migration. 146 type ToolsUploader interface { 147 UploadTools(io.ReadSeeker, version.Binary, ...string) (tools.List, error) 148 } 149 150 // ResourceDownloader defines the interface for downloading resources 151 // from the source controller during a migration. 152 type ResourceDownloader interface { 153 OpenResource(string, string) (io.ReadCloser, error) 154 } 155 156 // ResourceUploader defines the interface for uploading resources into 157 // the target controller during a migration. 158 type ResourceUploader interface { 159 UploadResource(resource.Resource, io.ReadSeeker) error 160 SetPlaceholderResource(resource.Resource) error 161 SetUnitResource(string, resource.Resource) error 162 } 163 164 // UploadBinariesConfig provides all the configuration that the 165 // UploadBinaries function needs to operate. To construct the config 166 // with the default helper functions, use `NewUploadBinariesConfig`. 167 type UploadBinariesConfig struct { 168 Charms []string 169 CharmDownloader CharmDownloader 170 CharmUploader CharmUploader 171 172 Tools map[version.Binary]string 173 ToolsDownloader ToolsDownloader 174 ToolsUploader ToolsUploader 175 176 Resources []migration.SerializedModelResource 177 ResourceDownloader ResourceDownloader 178 ResourceUploader ResourceUploader 179 } 180 181 // Validate makes sure that all the config values are non-nil. 182 func (c *UploadBinariesConfig) Validate() error { 183 if c.CharmDownloader == nil { 184 return errors.NotValidf("missing CharmDownloader") 185 } 186 if c.CharmUploader == nil { 187 return errors.NotValidf("missing CharmUploader") 188 } 189 if c.ToolsDownloader == nil { 190 return errors.NotValidf("missing ToolsDownloader") 191 } 192 if c.ToolsUploader == nil { 193 return errors.NotValidf("missing ToolsUploader") 194 } 195 if c.ResourceDownloader == nil { 196 return errors.NotValidf("missing ResourceDownloader") 197 } 198 if c.ResourceUploader == nil { 199 return errors.NotValidf("missing ResourceUploader") 200 } 201 return nil 202 } 203 204 // UploadBinaries will send binaries stored in the source blobstore to 205 // the target controller. 206 func UploadBinaries(config UploadBinariesConfig) error { 207 if err := config.Validate(); err != nil { 208 return errors.Trace(err) 209 } 210 if err := uploadCharms(config); err != nil { 211 return errors.Trace(err) 212 } 213 if err := uploadTools(config); err != nil { 214 return errors.Trace(err) 215 } 216 if err := uploadResources(config); err != nil { 217 return errors.Trace(err) 218 } 219 return nil 220 } 221 222 func streamThroughTempFile(r io.Reader) (_ io.ReadSeeker, cleanup func(), err error) { 223 tempFile, err := ioutil.TempFile("", "juju-migrate-binary") 224 if err != nil { 225 return nil, nil, errors.Trace(err) 226 } 227 defer func() { 228 if err != nil { 229 os.Remove(tempFile.Name()) 230 } 231 }() 232 _, err = io.Copy(tempFile, r) 233 if err != nil { 234 return nil, nil, errors.Trace(err) 235 } 236 tempFile.Seek(0, 0) 237 rmTempFile := func() { 238 filename := tempFile.Name() 239 tempFile.Close() 240 os.Remove(filename) 241 } 242 243 return tempFile, rmTempFile, nil 244 } 245 246 func uploadCharms(config UploadBinariesConfig) error { 247 // It is critical that charms are uploaded in ascending charm URL 248 // order so that charm revisions end up the same in the target as 249 // they were in the source. 250 naturalsort.Sort(config.Charms) 251 252 for _, charmURL := range config.Charms { 253 logger.Debugf("sending charm %s to target", charmURL) 254 255 curl, err := charm.ParseURL(charmURL) 256 if err != nil { 257 return errors.Annotate(err, "bad charm URL") 258 } 259 260 reader, err := config.CharmDownloader.OpenCharm(curl) 261 if err != nil { 262 return errors.Annotate(err, "cannot open charm") 263 } 264 defer reader.Close() 265 266 content, cleanup, err := streamThroughTempFile(reader) 267 if err != nil { 268 return errors.Trace(err) 269 } 270 defer cleanup() 271 272 if usedCurl, err := config.CharmUploader.UploadCharm(curl, content); err != nil { 273 return errors.Annotate(err, "cannot upload charm") 274 } else if usedCurl.String() != curl.String() { 275 // The target controller shouldn't assign a different charm URL. 276 return errors.Errorf("charm %s unexpectedly assigned %s", curl, usedCurl) 277 } 278 } 279 return nil 280 } 281 282 func uploadTools(config UploadBinariesConfig) error { 283 for v, uri := range config.Tools { 284 logger.Debugf("sending agent binaries to target: %s", v) 285 286 reader, err := config.ToolsDownloader.OpenURI(uri, nil) 287 if err != nil { 288 return errors.Annotate(err, "cannot open charm") 289 } 290 defer reader.Close() 291 292 content, cleanup, err := streamThroughTempFile(reader) 293 if err != nil { 294 return errors.Trace(err) 295 } 296 defer cleanup() 297 298 if _, err := config.ToolsUploader.UploadTools(content, v); err != nil { 299 return errors.Annotate(err, "cannot upload agent binaries") 300 } 301 } 302 return nil 303 } 304 305 func uploadResources(config UploadBinariesConfig) error { 306 for _, res := range config.Resources { 307 if res.ApplicationRevision.IsPlaceholder() { 308 // Resource placeholders created in the migration import rather 309 // than attempting to post empty resources. 310 } else { 311 err := uploadAppResource(config, res.ApplicationRevision) 312 if err != nil { 313 return errors.Trace(err) 314 } 315 } 316 for unitName, unitRev := range res.UnitRevisions { 317 if err := config.ResourceUploader.SetUnitResource(unitName, unitRev); err != nil { 318 return errors.Annotate(err, "cannot set unit resource") 319 } 320 } 321 // Each config.Resources element also contains a 322 // CharmStoreRevision field. This isn't especially important 323 // to migrate so is skipped for now. 324 } 325 return nil 326 } 327 328 func uploadAppResource(config UploadBinariesConfig, rev resource.Resource) error { 329 logger.Debugf("opening application resource for %s: %s", rev.ApplicationID, rev.Name) 330 reader, err := config.ResourceDownloader.OpenResource(rev.ApplicationID, rev.Name) 331 if err != nil { 332 return errors.Annotate(err, "cannot open resource") 333 } 334 defer reader.Close() 335 336 // TODO(menn0) - validate that the downloaded revision matches 337 // the expected metadata. Check revision and fingerprint. 338 339 content, cleanup, err := streamThroughTempFile(reader) 340 if err != nil { 341 return errors.Trace(err) 342 } 343 defer cleanup() 344 345 if err := config.ResourceUploader.UploadResource(rev, content); err != nil { 346 return errors.Annotate(err, "cannot upload resource") 347 } 348 return nil 349 }