github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/cloud/updateclouds.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package cloud 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "sort" 13 "strings" 14 15 "github.com/juju/cmd" 16 "github.com/juju/errors" 17 "github.com/juju/utils" 18 "github.com/juju/utils/set" 19 "golang.org/x/crypto/openpgp" 20 "golang.org/x/crypto/openpgp/clearsign" 21 22 jujucloud "github.com/juju/juju/cloud" 23 "github.com/juju/juju/juju/keys" 24 ) 25 26 type updateCloudsCommand struct { 27 cmd.CommandBase 28 29 publicSigningKey string 30 publicCloudURL string 31 } 32 33 var updateCloudsDoc = ` 34 If any new information for public clouds (such as regions and connection 35 endpoints) are available this command will update Juju accordingly. It is 36 suggested to run this command periodically. 37 38 Examples: 39 40 juju update-clouds 41 42 See also: 43 clouds 44 ` 45 46 // NewUpdateCloudsCommand returns a command to update cloud information. 47 var NewUpdateCloudsCommand = func() cmd.Command { 48 return newUpdateCloudsCommand() 49 } 50 51 func newUpdateCloudsCommand() cmd.Command { 52 return &updateCloudsCommand{ 53 publicSigningKey: keys.JujuPublicKey, 54 publicCloudURL: "https://streams.canonical.com/juju/public-clouds.syaml", 55 } 56 } 57 58 func (c *updateCloudsCommand) Info() *cmd.Info { 59 return &cmd.Info{ 60 Name: "update-clouds", 61 Purpose: "Updates public cloud information available to Juju.", 62 Doc: updateCloudsDoc, 63 } 64 } 65 66 func (c *updateCloudsCommand) Run(ctxt *cmd.Context) error { 67 fmt.Fprint(ctxt.Stderr, "Fetching latest public cloud list...\n") 68 client := utils.GetHTTPClient(utils.VerifySSLHostnames) 69 resp, err := client.Get(c.publicCloudURL) 70 if err != nil { 71 return err 72 } 73 defer resp.Body.Close() 74 75 if resp.StatusCode != http.StatusOK { 76 switch resp.StatusCode { 77 case http.StatusNotFound: 78 fmt.Fprintln(ctxt.Stderr, "Public cloud list is unavailable right now.") 79 return nil 80 case http.StatusUnauthorized: 81 return errors.Unauthorizedf("unauthorised access to URL %q", c.publicCloudURL) 82 } 83 return errors.Errorf("cannot read public cloud information at URL %q, %q", c.publicCloudURL, resp.Status) 84 } 85 86 cloudData, err := decodeCheckSignature(resp.Body, c.publicSigningKey) 87 if err != nil { 88 return errors.Annotate(err, "error receiving updated cloud data") 89 } 90 newPublicClouds, err := jujucloud.ParseCloudMetadata(cloudData) 91 if err != nil { 92 return errors.Annotate(err, "invalid cloud data received when updating clouds") 93 } 94 currentPublicClouds, _, err := jujucloud.PublicCloudMetadata(jujucloud.JujuPublicCloudsPath()) 95 if err != nil { 96 return errors.Annotate(err, "invalid local public cloud data") 97 } 98 sameCloudInfo, err := jujucloud.IsSameCloudMetadata(newPublicClouds, currentPublicClouds) 99 if err != nil { 100 // Should never happen. 101 return err 102 } 103 if sameCloudInfo { 104 fmt.Fprintln(ctxt.Stderr, "Your list of public clouds is up to date, see `juju clouds`.") 105 return nil 106 } 107 if err := jujucloud.WritePublicCloudMetadata(newPublicClouds); err != nil { 108 return errors.Annotate(err, "error writing new local public cloud data") 109 } 110 updateDetails := diffClouds(newPublicClouds, currentPublicClouds) 111 fmt.Fprintln(ctxt.Stderr, fmt.Sprintf("Updated your list of public clouds with %s", updateDetails)) 112 return nil 113 } 114 115 func decodeCheckSignature(r io.Reader, publicKey string) ([]byte, error) { 116 data, err := ioutil.ReadAll(r) 117 if err != nil { 118 return nil, err 119 } 120 b, _ := clearsign.Decode(data) 121 if b == nil { 122 return nil, errors.New("no PGP signature embedded in plain text data") 123 } 124 keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKey)) 125 if err != nil { 126 return nil, errors.Errorf("failed to parse public key: %v", err) 127 } 128 129 _, err = openpgp.CheckDetachedSignature(keyring, bytes.NewBuffer(b.Bytes), b.ArmoredSignature.Body) 130 if err != nil { 131 return nil, err 132 } 133 return b.Plaintext, nil 134 } 135 136 func diffClouds(newClouds, oldClouds map[string]jujucloud.Cloud) string { 137 diff := newChanges() 138 // added and updated clouds 139 for cloudName, cloud := range newClouds { 140 oldCloud, ok := oldClouds[cloudName] 141 if !ok { 142 diff.addChange(addChange, cloudScope, cloudName) 143 continue 144 } 145 146 if cloudChanged(cloudName, cloud, oldCloud) { 147 diffCloudDetails(cloudName, cloud, oldCloud, diff) 148 } 149 } 150 151 // deleted clouds 152 for cloudName, _ := range oldClouds { 153 if _, ok := newClouds[cloudName]; !ok { 154 diff.addChange(deleteChange, cloudScope, cloudName) 155 } 156 } 157 return diff.summary() 158 } 159 160 func cloudChanged(cloudName string, new, old jujucloud.Cloud) bool { 161 same, _ := jujucloud.IsSameCloudMetadata( 162 map[string]jujucloud.Cloud{cloudName: new}, 163 map[string]jujucloud.Cloud{cloudName: old}, 164 ) 165 // If both old and new version are the same the cloud is not changed. 166 return !same 167 } 168 169 func diffCloudDetails(cloudName string, new, old jujucloud.Cloud, diff *changes) { 170 sameAuthTypes := func() bool { 171 if len(old.AuthTypes) != len(new.AuthTypes) { 172 return false 173 } 174 newAuthTypes := set.NewStrings() 175 for _, one := range new.AuthTypes { 176 newAuthTypes.Add(string(one)) 177 } 178 179 for _, anOldOne := range old.AuthTypes { 180 if !newAuthTypes.Contains(string(anOldOne)) { 181 return false 182 } 183 } 184 return true 185 } 186 187 endpointChanged := new.Endpoint != old.Endpoint 188 identityEndpointChanged := new.IdentityEndpoint != old.IdentityEndpoint 189 storageEndpointChanged := new.StorageEndpoint != old.StorageEndpoint 190 191 if endpointChanged || identityEndpointChanged || storageEndpointChanged || new.Type != old.Type || !sameAuthTypes() { 192 diff.addChange(updateChange, attributeScope, cloudName) 193 } 194 195 formatCloudRegion := func(rName string) string { 196 return fmt.Sprintf("%v/%v", cloudName, rName) 197 } 198 199 oldRegions := mapRegions(old.Regions) 200 newRegions := mapRegions(new.Regions) 201 // added & modified regions 202 for newName, newRegion := range newRegions { 203 oldRegion, ok := oldRegions[newName] 204 if !ok { 205 diff.addChange(addChange, regionScope, formatCloudRegion(newName)) 206 continue 207 208 } 209 if (oldRegion.Endpoint != newRegion.Endpoint) || (oldRegion.IdentityEndpoint != newRegion.IdentityEndpoint) || (oldRegion.StorageEndpoint != newRegion.StorageEndpoint) { 210 diff.addChange(updateChange, regionScope, formatCloudRegion(newName)) 211 } 212 } 213 214 // deleted regions 215 for oldName, _ := range oldRegions { 216 if _, ok := newRegions[oldName]; !ok { 217 diff.addChange(deleteChange, regionScope, formatCloudRegion(oldName)) 218 } 219 } 220 } 221 222 func mapRegions(regions []jujucloud.Region) map[string]jujucloud.Region { 223 result := make(map[string]jujucloud.Region) 224 for _, region := range regions { 225 result[region.Name] = region 226 } 227 return result 228 } 229 230 type changeType string 231 232 const ( 233 addChange changeType = "added" 234 deleteChange changeType = "deleted" 235 updateChange changeType = "changed" 236 ) 237 238 type scope string 239 240 const ( 241 cloudScope scope = "cloud" 242 regionScope scope = "cloud region" 243 attributeScope scope = "cloud attribute" 244 ) 245 246 type changes struct { 247 all map[changeType]map[scope][]string 248 } 249 250 func newChanges() *changes { 251 return &changes{make(map[changeType]map[scope][]string)} 252 } 253 254 func (c *changes) addChange(aType changeType, entity scope, details string) { 255 byType, ok := c.all[aType] 256 if !ok { 257 byType = make(map[scope][]string) 258 c.all[aType] = byType 259 } 260 byType[entity] = append(byType[entity], details) 261 } 262 263 func (c *changes) summary() string { 264 if len(c.all) == 0 { 265 return "" 266 } 267 268 // Sort by change types 269 types := []string{} 270 for one, _ := range c.all { 271 types = append(types, string(one)) 272 } 273 sort.Strings(types) 274 275 msgs := []string{} 276 details := "" 277 tabSpace := " " 278 detailsSeparator := fmt.Sprintf("\n%v%v- ", tabSpace, tabSpace) 279 for _, aType := range types { 280 typeGroup := c.all[changeType(aType)] 281 entityMsgs := []string{} 282 283 // Sort by change scopes 284 scopes := []string{} 285 for one, _ := range typeGroup { 286 scopes = append(scopes, string(one)) 287 } 288 sort.Strings(scopes) 289 290 for _, aScope := range scopes { 291 scopeGroup := typeGroup[scope(aScope)] 292 sort.Strings(scopeGroup) 293 entityMsgs = append(entityMsgs, adjustPlurality(aScope, len(scopeGroup))) 294 details += fmt.Sprintf("\n%v%v %v:%v%v", 295 tabSpace, 296 aType, 297 aScope, 298 detailsSeparator, 299 strings.Join(scopeGroup, detailsSeparator)) 300 } 301 typeMsg := formatSlice(entityMsgs, ", ", " and ") 302 msgs = append(msgs, fmt.Sprintf("%v %v", typeMsg, aType)) 303 } 304 305 result := formatSlice(msgs, "; ", " as well as ") 306 return fmt.Sprintf("%v:\n%v", result, details) 307 } 308 309 // TODO(anastasiamac 2014-04-13) Move this to 310 // juju/utils (eg. Pluralize). Added tech debt card. 311 func adjustPlurality(entity string, count int) string { 312 switch count { 313 case 0: 314 return "" 315 case 1: 316 return fmt.Sprintf("%d %v", count, entity) 317 default: 318 return fmt.Sprintf("%d %vs", count, entity) 319 } 320 } 321 322 func formatSlice(slice []string, itemSeparator, lastSeparator string) string { 323 switch len(slice) { 324 case 0: 325 return "" 326 case 1: 327 return slice[0] 328 default: 329 return fmt.Sprintf("%v%v%v", 330 strings.Join(slice[:len(slice)-1], itemSeparator), 331 lastSeparator, 332 slice[len(slice)-1]) 333 } 334 }