github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/provider/ec2/environ_vpc.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package ec2 5 6 import ( 7 "fmt" 8 9 "github.com/juju/errors" 10 "github.com/juju/utils/set" 11 "gopkg.in/amz.v3/ec2" 12 13 "github.com/juju/juju/network" 14 ) 15 16 const ( 17 activeState = "active" 18 availableState = "available" 19 localRouteGatewayID = "local" 20 defaultRouteCIDRBlock = "0.0.0.0/0" 21 defaultVPCIDNone = "none" 22 ) 23 24 var ( 25 vpcNotUsableErrorPrefix = ` 26 Juju cannot use the given vpc-id for bootstrapping a controller 27 instance. Please, double check the given VPC ID is correct, and that 28 the VPC contains at least one subnet. 29 30 Error details`[1:] 31 32 vpcNotRecommendedErrorPrefix = ` 33 The given vpc-id does not meet one or more of the following minimum 34 Juju requirements: 35 36 1. VPC should be in "available" state and contain one or more subnets. 37 2. An Internet Gateway (IGW) should be attached to the VPC. 38 3. The main route table of the VPC should have both a default route 39 to the attached IGW and a local route matching the VPC CIDR block. 40 4. At least one of the VPC subnets should have MapPublicIPOnLaunch 41 attribute enabled (i.e. at least one subnet needs to be 'public'). 42 5. All subnets should be implicitly associated to the VPC main route 43 table, rather than explicitly to per-subnet route tables. 44 45 A default VPC already satisfies all of the requirements above. If you 46 still want to use the VPC, try running 'juju bootstrap' again with: 47 48 --config vpc-id=%s --config vpc-id-force=true 49 50 to force Juju to bypass the requirements check (NOT recommended unless 51 you understand the implications: most importantly, not being able to 52 access the Juju controller, likely causing bootstrap to fail, or trying 53 to deploy exposed workloads on instances started in private or isolated 54 subnets). 55 56 Error details`[1:] 57 58 cannotValidateVPCErrorPrefix = ` 59 Juju could not verify whether the given vpc-id meets the minumum Juju 60 connectivity requirements. Please, double check the VPC ID is correct, 61 you have a working connection to the Internet, your AWS credentials are 62 sufficient to access VPC features, or simply retry bootstrapping again. 63 64 Error details`[1:] 65 66 vpcNotRecommendedButForcedWarning = ` 67 WARNING! The specified vpc-id does not satisfy the minimum Juju requirements, 68 but will be used anyway because vpc-id-force=true is also specified. 69 70 `[1:] 71 ) 72 73 // vpcNotUsableError indicates a user-specified VPC cannot be used either 74 // because it is missing or because it contains no subnets. 75 type vpcNotUsableError struct { 76 errors.Err 77 } 78 79 // vpcNotUsablef returns an error which satisfies isVPCNotUsableError(). 80 func vpcNotUsablef(optionalCause error, format string, args ...interface{}) error { 81 outerErr := errors.Errorf(format, args...) 82 if optionalCause != nil { 83 outerErr = errors.Maskf(optionalCause, format, args...) 84 } 85 86 innerErr, _ := outerErr.(*errors.Err) // cannot fail. 87 return &vpcNotUsableError{*innerErr} 88 } 89 90 // isVPCNotUsableError reports whether err was created with vpcNotUsablef(). 91 func isVPCNotUsableError(err error) bool { 92 err = errors.Cause(err) 93 _, ok := err.(*vpcNotUsableError) 94 return ok 95 } 96 97 // vpcNotRecommendedError indicates a user-specified VPC is unlikely to be 98 // suitable for hosting a Juju controller instance and/or exposed workloads, due 99 // to not satisfying the mininum requirements described in validateVPC()'s doc 100 // comment. Users can still force Juju to use such a VPC by passing 101 // 'vpc-id-force=true' setting. 102 type vpcNotRecommendedError struct { 103 errors.Err 104 } 105 106 // vpcNotRecommendedf returns an error which satisfies isVPCNotRecommendedError(). 107 func vpcNotRecommendedf(format string, args ...interface{}) error { 108 outerErr := errors.Errorf(format, args...) 109 innerErr, _ := outerErr.(*errors.Err) // cannot fail. 110 return &vpcNotRecommendedError{*innerErr} 111 } 112 113 // isVPCNotRecommendedError reports whether err was created with vpcNotRecommendedf(). 114 func isVPCNotRecommendedError(err error) bool { 115 err = errors.Cause(err) 116 _, ok := err.(*vpcNotRecommendedError) 117 return ok 118 } 119 120 // vpcAPIClient defines a subset of the goamz API calls needed to validate a VPC. 121 type vpcAPIClient interface { 122 // AccountAttributes, called with the "default-vpc" attribute. is used to 123 // find the ID of the region's default VPC (if any). 124 AccountAttributes(attributeNames ...string) (*ec2.AccountAttributesResp, error) 125 126 // VPCs is used to get details for the VPC being validated, including 127 // whether it exists, is available, and its CIDRBlock and IsDefault fields. 128 VPCs(ids []string, filter *ec2.Filter) (*ec2.VPCsResp, error) 129 130 // Subnets is used to get a list of all subnets of the validated VPC (if 131 // any),including their Id, AvailZone, and MapPublicIPOnLaunch fields. 132 Subnets(ids []string, filter *ec2.Filter) (*ec2.SubnetsResp, error) 133 134 // InternetGateways is used to get details of the Internet Gateway (IGW) 135 // attached to the validated VPC (if any), its Id to check against routes, 136 // and whether it's available. 137 InternetGateways(ids []string, filter *ec2.Filter) (*ec2.InternetGatewaysResp, error) 138 139 // RouteTables is used to find the main route table of the VPC (if any), 140 // whether it includes a default route to the attached IGW, a local route to 141 // the VPC CIDRBlock, and any per-subnet route tables. 142 RouteTables(ids []string, filter *ec2.Filter) (*ec2.RouteTablesResp, error) 143 } 144 145 // validateVPC requires both arguments to be set and validates that vpcID refers 146 // to an existing AWS VPC (default or non-default) for the current region. 147 // Returns an error satifying isVPCNotUsableError() when the VPC with the given 148 // vpcID cannot be found, or when the VPC exists but contains no subnets. 149 // Returns an error satisfying isVPCNotRecommendedError() in the following 150 // cases: 151 // 152 // 1. The VPC's state is not "available". 153 // 2. The VPC does not have an Internet Gateway (IGW) attached. 154 // 3. A main route table is not associated with the VPC. 155 // 4. The main route table lacks both a default route via the IGW and a local 156 // route matching the VPC's CIDR block. 157 // 5. One or more of the VPC's subnets are not associated with the main route 158 // table of the VPC. 159 // 6. None of the the VPC's subnets have the MapPublicIPOnLaunch attribute set. 160 // 161 // With the vpc-id-force config setting set to true, the provider can ignore a 162 // vpcNotRecommendedError. A vpcNotUsableError cannot be ignored, while 163 // unexpected API responses and errors could be retried. 164 // 165 // The above minimal requirements allow Juju to work out-of-the-box with most 166 // common (and officially documented by AWS) VPC setups, easy try out with AWS 167 // Console / VPC Wizard / CLI. Detecting VPC setups indicating intentional 168 // customization by experienced users, protecting beginners from bad Juju-UX due 169 // to broken VPC setup, while still allowing power users to override that and 170 // continue (but knowing what that implies). 171 func validateVPC(apiClient vpcAPIClient, vpcID string) error { 172 if vpcID == "" || apiClient == nil { 173 return errors.Errorf("invalid arguments: empty VPC ID or nil client") 174 } 175 176 vpc, err := getVPCByID(apiClient, vpcID) 177 if err != nil { 178 return errors.Trace(err) 179 } 180 181 if err := checkVPCIsAvailable(vpc); err != nil { 182 return errors.Trace(err) 183 } 184 185 subnets, err := getVPCSubnets(apiClient, vpc) 186 if err != nil { 187 return errors.Trace(err) 188 } 189 190 publicSubnet, err := findFirstPublicSubnet(subnets) 191 if err != nil { 192 return errors.Trace(err) 193 } 194 195 // TODO(dimitern): Rather than just logging that, use publicSubnet.Id or 196 // even publicSubnet.AvailZone as default bootstrap placement directive, so 197 // the controller would be reachable. 198 logger.Infof( 199 "found subnet %q (%s) in AZ %q, suitable for a Juju controller instance", 200 publicSubnet.Id, publicSubnet.CIDRBlock, publicSubnet.AvailZone, 201 ) 202 203 gateway, err := getVPCInternetGateway(apiClient, vpc) 204 if err != nil { 205 return errors.Trace(err) 206 } 207 208 if err := checkInternetGatewayIsAvailable(gateway); err != nil { 209 return errors.Trace(err) 210 } 211 212 routeTables, err := getVPCRouteTables(apiClient, vpc) 213 if err != nil { 214 return errors.Trace(err) 215 } 216 217 mainRouteTable, err := findVPCMainRouteTable(routeTables) 218 if err != nil { 219 return errors.Trace(err) 220 } 221 222 if err := checkVPCRouteTableRoutes(vpc, mainRouteTable, gateway); err != nil { 223 return errors.Annotatef(err, "VPC %q main route table %q", vpcID, mainRouteTable.Id) 224 } 225 226 logger.Infof("VPC %q is suitable for Juju controllers and expose-able workloads", vpc.Id) 227 return nil 228 } 229 230 func getVPCByID(apiClient vpcAPIClient, vpcID string) (*ec2.VPC, error) { 231 response, err := apiClient.VPCs([]string{vpcID}, nil) 232 if isVPCNotFoundError(err) { 233 return nil, vpcNotUsablef(err, "") 234 } else if err != nil { 235 return nil, errors.Annotatef(err, "unexpected AWS response getting VPC %q", vpcID) 236 } 237 238 if numResults := len(response.VPCs); numResults == 0 { 239 return nil, vpcNotUsablef(nil, "VPC %q not found", vpcID) 240 } else if numResults > 1 { 241 logger.Debugf("VPCs() returned %#v", response) 242 return nil, errors.Errorf("expected 1 result from AWS, got %d", numResults) 243 } 244 245 vpc := response.VPCs[0] 246 return &vpc, nil 247 } 248 249 func isVPCNotFoundError(err error) bool { 250 return err != nil && ec2ErrCode(err) == "InvalidVpcID.NotFound" 251 } 252 253 func checkVPCIsAvailable(vpc *ec2.VPC) error { 254 if vpc.State != availableState { 255 return vpcNotRecommendedf("VPC has unexpected state %q", vpc.State) 256 } 257 258 if vpc.IsDefault { 259 logger.Infof("VPC %q is the default VPC for the region", vpc.Id) 260 } 261 262 return nil 263 } 264 265 func getVPCSubnets(apiClient vpcAPIClient, vpc *ec2.VPC) ([]ec2.Subnet, error) { 266 filter := ec2.NewFilter() 267 filter.Add("vpc-id", vpc.Id) 268 response, err := apiClient.Subnets(nil, filter) 269 if err != nil { 270 return nil, errors.Annotatef(err, "unexpected AWS response getting subnets of VPC %q", vpc.Id) 271 } 272 273 if len(response.Subnets) == 0 { 274 return nil, vpcNotUsablef(nil, "no subnets found for VPC %q", vpc.Id) 275 } 276 277 return response.Subnets, nil 278 } 279 280 func findFirstPublicSubnet(subnets []ec2.Subnet) (*ec2.Subnet, error) { 281 for _, subnet := range subnets { 282 if subnet.MapPublicIPOnLaunch { 283 return &subnet, nil 284 } 285 286 } 287 return nil, vpcNotRecommendedf("VPC contains no public subnets") 288 } 289 290 func getVPCInternetGateway(apiClient vpcAPIClient, vpc *ec2.VPC) (*ec2.InternetGateway, error) { 291 filter := ec2.NewFilter() 292 filter.Add("attachment.vpc-id", vpc.Id) 293 response, err := apiClient.InternetGateways(nil, filter) 294 if err != nil { 295 return nil, errors.Annotatef(err, "unexpected AWS response getting Internet Gateway of VPC %q", vpc.Id) 296 } 297 298 if numResults := len(response.InternetGateways); numResults == 0 { 299 return nil, vpcNotRecommendedf("VPC has no Internet Gateway attached") 300 } else if numResults > 1 { 301 logger.Debugf("InternetGateways() returned %#v", response) 302 return nil, errors.Errorf("expected 1 result from AWS, got %d", numResults) 303 } 304 305 gateway := response.InternetGateways[0] 306 return &gateway, nil 307 } 308 309 func checkInternetGatewayIsAvailable(gateway *ec2.InternetGateway) error { 310 if state := gateway.AttachmentState; state != availableState { 311 return vpcNotRecommendedf("VPC has Internet Gateway %q in unexpected state %q", gateway.Id, state) 312 } 313 314 return nil 315 } 316 317 func getVPCRouteTables(apiClient vpcAPIClient, vpc *ec2.VPC) ([]ec2.RouteTable, error) { 318 filter := ec2.NewFilter() 319 filter.Add("vpc-id", vpc.Id) 320 response, err := apiClient.RouteTables(nil, filter) 321 if err != nil { 322 return nil, errors.Annotatef(err, "unexpected AWS response getting route tables of VPC %q", vpc.Id) 323 } 324 325 if len(response.Tables) == 0 { 326 return nil, vpcNotRecommendedf("VPC has no route tables") 327 } 328 logger.Tracef("RouteTables() returned %#v", response) 329 330 return response.Tables, nil 331 } 332 333 func findVPCMainRouteTable(routeTables []ec2.RouteTable) (*ec2.RouteTable, error) { 334 var mainTable *ec2.RouteTable 335 for i, table := range routeTables { 336 if len(table.Associations) < 1 { 337 logger.Tracef("ignoring VPC %q route table %q with no associations", table.VPCId, table.Id) 338 continue 339 } 340 341 for _, association := range table.Associations { 342 // TODO(dimitern): Of all the requirements, this seems like the most 343 // strict and likely to push users to use vpc-id-force=true. On the 344 // other hand, having to deal with more than the main route table's 345 // routes will likely overcomplicate the routes checks that follow. 346 if subnetID := association.SubnetId; subnetID != "" { 347 return nil, vpcNotRecommendedf("subnet %q not associated with VPC %q main route table", subnetID, table.VPCId) 348 } 349 350 if association.IsMain && mainTable == nil { 351 logger.Tracef("main route table of VPC %q has ID %q", table.VPCId, table.Id) 352 mainTable = &routeTables[i] 353 } 354 } 355 } 356 357 if mainTable == nil { 358 return nil, vpcNotRecommendedf("VPC has no associated main route table") 359 } 360 361 return mainTable, nil 362 } 363 364 func checkVPCRouteTableRoutes(vpc *ec2.VPC, routeTable *ec2.RouteTable, gateway *ec2.InternetGateway) error { 365 hasDefaultRoute := false 366 hasLocalRoute := false 367 368 logger.Tracef("checking route table %+v routes", routeTable) 369 for _, route := range routeTable.Routes { 370 if route.State != activeState { 371 logger.Tracef("skipping inactive route %+v", route) 372 continue 373 } 374 375 switch route.DestinationCIDRBlock { 376 case defaultRouteCIDRBlock: 377 if route.GatewayId == gateway.Id { 378 logger.Tracef("default route uses expected gateway %q", gateway.Id) 379 hasDefaultRoute = true 380 } 381 case vpc.CIDRBlock: 382 if route.GatewayId == localRouteGatewayID { 383 logger.Tracef("local route uses expected CIDR %q", vpc.CIDRBlock) 384 hasLocalRoute = true 385 } 386 default: 387 logger.Tracef("route %+v is neither local nor default (skipping)", route) 388 } 389 } 390 391 if hasDefaultRoute && hasLocalRoute { 392 return nil 393 } 394 395 if !hasDefaultRoute { 396 return vpcNotRecommendedf("missing default route via gateway %q", gateway.Id) 397 } 398 return vpcNotRecommendedf("missing local route with destination %q", vpc.CIDRBlock) 399 } 400 401 func findDefaultVPCID(apiClient vpcAPIClient) (string, error) { 402 response, err := apiClient.AccountAttributes("default-vpc") 403 if err != nil { 404 return "", errors.Annotate(err, "unexpected AWS response getting default-vpc account attribute") 405 } 406 407 if len(response.Attributes) == 0 || 408 len(response.Attributes[0].Values) == 0 || 409 response.Attributes[0].Name != "default-vpc" { 410 // No value for the requested "default-vpc" attribute, all bets are off. 411 return "", errors.NotFoundf("default-vpc account attribute") 412 } 413 414 firstAttributeValue := response.Attributes[0].Values[0] 415 if firstAttributeValue == defaultVPCIDNone { 416 return "", errors.NotFoundf("default VPC") 417 } 418 419 return firstAttributeValue, nil 420 } 421 422 // getVPCSubnetIDsForAvailabilityZone returns a list of subnet IDs, which are 423 // both in the given vpcID and the given zoneName. Returns an error satisfying 424 // errors.IsNotFound() otherwise. 425 func getVPCSubnetIDsForAvailabilityZone(apiClient vpcAPIClient, vpcID, zoneName string) ([]string, error) { 426 vpc := &ec2.VPC{Id: vpcID} 427 subnets, err := getVPCSubnets(apiClient, vpc) 428 if err != nil && !isVPCNotUsableError(err) { 429 return nil, errors.Annotatef(err, "cannot get VPC %q subnets", vpcID) 430 } else if isVPCNotUsableError(err) { 431 // We're reusing getVPCSubnets(), but not while validating a VPC 432 // pre-bootstrap, so we should change vpcNotUsableError to a simple 433 // NotFoundError. 434 message := fmt.Sprintf("VPC %q has no subnets in AZ %q", vpcID, zoneName) 435 return nil, errors.NewNotFound(err, message) 436 } 437 438 matchingSubnetIDs := set.NewStrings() 439 for _, subnet := range subnets { 440 if subnet.AvailZone == zoneName { 441 matchingSubnetIDs.Add(subnet.Id) 442 } 443 } 444 445 if matchingSubnetIDs.IsEmpty() { 446 message := fmt.Sprintf("VPC %q has no subnets in AZ %q", vpcID, zoneName) 447 return nil, errors.NewNotFound(nil, message) 448 } 449 450 return matchingSubnetIDs.SortedValues(), nil 451 } 452 453 func findSubnetIDsForAvailabilityZone(zoneName string, subnetsToZones map[network.Id][]string) ([]string, error) { 454 matchingSubnetIDs := set.NewStrings() 455 for subnetID, zones := range subnetsToZones { 456 zonesSet := set.NewStrings(zones...) 457 if zonesSet.Contains(zoneName) { 458 matchingSubnetIDs.Add(string(subnetID)) 459 } 460 } 461 462 if matchingSubnetIDs.IsEmpty() { 463 return nil, errors.NotFoundf("subnets in AZ %q", zoneName) 464 } 465 466 return matchingSubnetIDs.SortedValues(), nil 467 }