github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/azure/instance.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package azure 5 6 import ( 7 stdcontext "context" 8 "fmt" 9 "net/http" 10 "strings" 11 12 "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-08-01/network" 13 "github.com/Azure/go-autorest/autorest" 14 "github.com/Azure/go-autorest/autorest/to" 15 "github.com/juju/errors" 16 "gopkg.in/juju/names.v2" 17 18 "github.com/juju/juju/core/instance" 19 corenetwork "github.com/juju/juju/core/network" 20 "github.com/juju/juju/core/status" 21 "github.com/juju/juju/environs/context" 22 jujunetwork "github.com/juju/juju/network" 23 "github.com/juju/juju/provider/azure/internal/errorutils" 24 ) 25 26 type azureInstance struct { 27 vmName string 28 provisioningState string 29 env *azureEnviron 30 networkInterfaces []network.Interface 31 publicIPAddresses []network.PublicIPAddress 32 } 33 34 // Id is specified in the Instance interface. 35 func (inst *azureInstance) Id() instance.Id { 36 // Note: we use Name and not Id, since all VM operations are in 37 // terms of the VM name (qualified by resource group). The ID is 38 // an internal detail. 39 return instance.Id(inst.vmName) 40 } 41 42 // Status is specified in the Instance interface. 43 func (inst *azureInstance) Status(ctx context.ProviderCallContext) instance.Status { 44 instanceStatus := status.Empty 45 message := inst.provisioningState 46 switch inst.provisioningState { 47 case "Succeeded": 48 // TODO(axw) once a VM has been started, we should 49 // start using its power state to show if it's 50 // really running or not. This is just a nice to 51 // have, since we should not expect a VM to ever 52 // be stopped. 53 instanceStatus = status.Running 54 message = "" 55 case "Canceled", "Failed": 56 // TODO(axw) if the provisioning state is "Failed", then we 57 // should use the error message from the deployment description 58 // as the Message. The error details are not currently exposed 59 // in the Azure SDK. See: 60 // https://github.com/Azure/azure-sdk-for-go/issues/399 61 instanceStatus = status.ProvisioningError 62 case "Running": 63 message = "" 64 fallthrough 65 default: 66 instanceStatus = status.Provisioning 67 } 68 return instance.Status{ 69 Status: instanceStatus, 70 Message: message, 71 } 72 } 73 74 // setInstanceAddresses queries Azure for the NICs and public IPs associated 75 // with the given set of instances. This assumes that the instances' 76 // VirtualMachines are up-to-date, and that there are no concurrent accesses 77 // to the instances. 78 func setInstanceAddresses( 79 ctx context.ProviderCallContext, 80 resourceGroup string, 81 nicClient network.InterfacesClient, 82 pipClient network.PublicIPAddressesClient, 83 instances []*azureInstance, 84 ) (err error) { 85 instanceNics, err := instanceNetworkInterfaces(ctx, resourceGroup, nicClient) 86 if err != nil { 87 return errors.Annotate(err, "listing network interfaces") 88 } 89 instancePips, err := instancePublicIPAddresses(ctx, resourceGroup, pipClient) 90 if err != nil { 91 return errors.Annotate(err, "listing public IP addresses") 92 } 93 for _, inst := range instances { 94 inst.networkInterfaces = instanceNics[inst.Id()] 95 inst.publicIPAddresses = instancePips[inst.Id()] 96 } 97 return nil 98 } 99 100 // instanceNetworkInterfaces lists all network interfaces in the resource 101 // group, and returns a mapping from instance ID to the network interfaces 102 // associated with that instance. 103 func instanceNetworkInterfaces( 104 ctx context.ProviderCallContext, 105 resourceGroup string, 106 nicClient network.InterfacesClient, 107 ) (map[instance.Id][]network.Interface, error) { 108 sdkCtx := stdcontext.Background() 109 nicsResult, err := nicClient.ListComplete(sdkCtx, resourceGroup) 110 if err != nil { 111 return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing network interfaces"), ctx) 112 } 113 if nicsResult.Response().IsEmpty() { 114 return nil, nil 115 } 116 instanceNics := make(map[instance.Id][]network.Interface) 117 for ; nicsResult.NotDone(); err = nicsResult.NextWithContext(sdkCtx) { 118 nic := nicsResult.Value() 119 instanceId := instance.Id(to.String(nic.Tags[jujuMachineNameTag])) 120 instanceNics[instanceId] = append(instanceNics[instanceId], nic) 121 } 122 return instanceNics, nil 123 } 124 125 // interfacePublicIPAddresses lists all public IP addresses in the resource 126 // group, and returns a mapping from instance ID to the public IP addresses 127 // associated with that instance. 128 func instancePublicIPAddresses( 129 ctx context.ProviderCallContext, 130 resourceGroup string, 131 pipClient network.PublicIPAddressesClient, 132 ) (map[instance.Id][]network.PublicIPAddress, error) { 133 sdkCtx := stdcontext.Background() 134 pipsResult, err := pipClient.ListComplete(sdkCtx, resourceGroup) 135 if err != nil { 136 return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing public IP addresses"), ctx) 137 } 138 if pipsResult.Response().IsEmpty() { 139 return nil, nil 140 } 141 instancePips := make(map[instance.Id][]network.PublicIPAddress) 142 for ; pipsResult.NotDone(); err = pipsResult.NextWithContext(sdkCtx) { 143 pip := pipsResult.Value() 144 instanceId := instance.Id(to.String(pip.Tags[jujuMachineNameTag])) 145 instancePips[instanceId] = append(instancePips[instanceId], pip) 146 } 147 return instancePips, nil 148 } 149 150 // Addresses is specified in the Instance interface. 151 func (inst *azureInstance) Addresses(ctx context.ProviderCallContext) ([]jujunetwork.Address, error) { 152 addresses := make([]jujunetwork.Address, 0, len(inst.networkInterfaces)+len(inst.publicIPAddresses)) 153 for _, nic := range inst.networkInterfaces { 154 if nic.IPConfigurations == nil { 155 continue 156 } 157 for _, ipConfiguration := range *nic.IPConfigurations { 158 privateIpAddress := ipConfiguration.PrivateIPAddress 159 if privateIpAddress == nil { 160 continue 161 } 162 addresses = append(addresses, jujunetwork.NewScopedAddress( 163 to.String(privateIpAddress), 164 jujunetwork.ScopeCloudLocal, 165 )) 166 } 167 } 168 for _, pip := range inst.publicIPAddresses { 169 if pip.IPAddress == nil { 170 continue 171 } 172 addresses = append(addresses, jujunetwork.NewScopedAddress( 173 to.String(pip.IPAddress), 174 jujunetwork.ScopePublic, 175 )) 176 } 177 return addresses, nil 178 } 179 180 // primaryNetworkAddress returns the instance's primary jujunetwork.Address for 181 // the internal virtual network. This address is used to identify the machine in 182 // network security rules. 183 func (inst *azureInstance) primaryNetworkAddress() (jujunetwork.Address, error) { 184 for _, nic := range inst.networkInterfaces { 185 if nic.IPConfigurations == nil { 186 continue 187 } 188 for _, ipConfiguration := range *nic.IPConfigurations { 189 if ipConfiguration.Subnet == nil { 190 continue 191 } 192 if !to.Bool(ipConfiguration.Primary) { 193 continue 194 } 195 privateIpAddress := ipConfiguration.PrivateIPAddress 196 if privateIpAddress == nil { 197 continue 198 } 199 return jujunetwork.NewScopedAddress( 200 to.String(privateIpAddress), 201 jujunetwork.ScopeCloudLocal, 202 ), nil 203 } 204 } 205 return jujunetwork.Address{}, errors.NotFoundf("internal network address") 206 } 207 208 // OpenPorts is specified in the Instance interface. 209 func (inst *azureInstance) OpenPorts(ctx context.ProviderCallContext, machineId string, rules []jujunetwork.IngressRule) error { 210 nsgClient := network.SecurityGroupsClient{inst.env.network} 211 securityRuleClient := network.SecurityRulesClient{inst.env.network} 212 primaryNetworkAddress, err := inst.primaryNetworkAddress() 213 if err != nil { 214 return errors.Trace(err) 215 } 216 217 securityGroupName := internalSecurityGroupName 218 sdkCtx := stdcontext.Background() 219 nsg, err := nsgClient.Get(sdkCtx, inst.env.resourceGroup, securityGroupName, "") 220 if err != nil { 221 return errorutils.HandleCredentialError(errors.Annotate(err, "querying network security group"), ctx) 222 } 223 224 var securityRules []network.SecurityRule 225 if nsg.SecurityRules != nil { 226 securityRules = *nsg.SecurityRules 227 } else { 228 nsg.SecurityRules = &securityRules 229 } 230 231 // Create rules one at a time; this is necessary to avoid trampling 232 // on changes made by the provisioner. We still record rules in the 233 // NSG in memory, so we can easily tell which priorities are available. 234 vmName := resourceName(names.NewMachineTag(machineId)) 235 prefix := instanceNetworkSecurityRulePrefix(instance.Id(vmName)) 236 237 singleSourceIngressRules := explodeIngressRules(rules) 238 for _, rule := range singleSourceIngressRules { 239 ruleName := securityRuleName(prefix, rule) 240 241 // Check if the rule already exists; OpenPorts must be idempotent. 242 var found bool 243 for _, rule := range securityRules { 244 if to.String(rule.Name) == ruleName { 245 found = true 246 break 247 } 248 } 249 if found { 250 logger.Debugf("security rule %q already exists", ruleName) 251 continue 252 } 253 logger.Debugf("creating security rule %q", ruleName) 254 255 priority, err := nextSecurityRulePriority(nsg, securityRuleInternalMax+1, securityRuleMax) 256 if err != nil { 257 return errors.Annotatef(err, "getting security rule priority for %q", rule) 258 } 259 260 var protocol network.SecurityRuleProtocol 261 switch rule.Protocol { 262 case "tcp": 263 protocol = network.SecurityRuleProtocolTCP 264 case "udp": 265 protocol = network.SecurityRuleProtocolUDP 266 default: 267 return errors.Errorf("invalid protocol %q", rule.Protocol) 268 } 269 270 var portRange string 271 if rule.FromPort != rule.ToPort { 272 portRange = fmt.Sprintf("%d-%d", rule.FromPort, rule.ToPort) 273 } else { 274 portRange = fmt.Sprint(rule.FromPort) 275 } 276 277 // rule has a single source CIDR 278 from := rule.SourceCIDRs[0] 279 securityRule := network.SecurityRule{ 280 SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{ 281 Description: to.StringPtr(rule.String()), 282 Protocol: protocol, 283 SourcePortRange: to.StringPtr("*"), 284 DestinationPortRange: to.StringPtr(portRange), 285 SourceAddressPrefix: to.StringPtr(from), 286 DestinationAddressPrefix: to.StringPtr(primaryNetworkAddress.Value), 287 Access: network.SecurityRuleAccessAllow, 288 Priority: to.Int32Ptr(priority), 289 Direction: network.SecurityRuleDirectionInbound, 290 }, 291 } 292 _, err = securityRuleClient.CreateOrUpdate( 293 sdkCtx, 294 inst.env.resourceGroup, securityGroupName, ruleName, securityRule, 295 ) 296 if err != nil { 297 return errorutils.HandleCredentialError(errors.Annotatef(err, "creating security rule for %q", ruleName), ctx) 298 } 299 securityRules = append(securityRules, securityRule) 300 } 301 return nil 302 } 303 304 // ClosePorts is specified in the Instance interface. 305 func (inst *azureInstance) ClosePorts(ctx context.ProviderCallContext, machineId string, rules []jujunetwork.IngressRule) error { 306 securityRuleClient := network.SecurityRulesClient{inst.env.network} 307 securityGroupName := internalSecurityGroupName 308 309 // Delete rules one at a time; this is necessary to avoid trampling 310 // on changes made by the provisioner. 311 vmName := resourceName(names.NewMachineTag(machineId)) 312 prefix := instanceNetworkSecurityRulePrefix(instance.Id(vmName)) 313 sdkCtx := stdcontext.Background() 314 315 singleSourceIngressRules := explodeIngressRules(rules) 316 for _, rule := range singleSourceIngressRules { 317 ruleName := securityRuleName(prefix, rule) 318 logger.Debugf("deleting security rule %q", ruleName) 319 future, err := securityRuleClient.Delete( 320 stdcontext.Background(), 321 inst.env.resourceGroup, securityGroupName, ruleName, 322 ) 323 if err != nil { 324 if !isNotFoundResponse(future.Response()) { 325 return errors.Annotatef(err, "deleting security rule %q", ruleName) 326 } 327 continue 328 } 329 err = future.WaitForCompletionRef(sdkCtx, securityRuleClient.Client) 330 if err != nil { 331 return errors.Annotatef(err, "deleting security rule %q", ruleName) 332 } 333 result, err := future.Result(securityRuleClient) 334 if err != nil && !isNotFoundResult(result) { 335 return errorutils.HandleCredentialError(errors.Annotatef(err, "deleting security rule %q", ruleName), ctx) 336 } 337 } 338 return nil 339 } 340 341 // IngressRules is specified in the Instance interface. 342 func (inst *azureInstance) IngressRules(ctx context.ProviderCallContext, machineId string) (rules []jujunetwork.IngressRule, err error) { 343 nsgClient := network.SecurityGroupsClient{inst.env.network} 344 securityGroupName := internalSecurityGroupName 345 nsg, err := nsgClient.Get(stdcontext.Background(), inst.env.resourceGroup, securityGroupName, "") 346 if err != nil { 347 return nil, errorutils.HandleCredentialError(errors.Annotate(err, "querying network security group"), ctx) 348 } 349 if nsg.SecurityRules == nil { 350 return nil, nil 351 } 352 353 vmName := resourceName(names.NewMachineTag(machineId)) 354 prefix := instanceNetworkSecurityRulePrefix(instance.Id(vmName)) 355 356 // Keep track of all the SourceAddressPrefixes for each port range. 357 portSourceCIDRs := make(map[corenetwork.PortRange]*[]string) 358 for _, rule := range *nsg.SecurityRules { 359 if rule.Direction != network.SecurityRuleDirectionInbound { 360 continue 361 } 362 if rule.Access != network.SecurityRuleAccessAllow { 363 continue 364 } 365 if to.Int32(rule.Priority) <= securityRuleInternalMax { 366 continue 367 } 368 if !strings.HasPrefix(to.String(rule.Name), prefix) { 369 continue 370 } 371 372 var portRange corenetwork.PortRange 373 if *rule.DestinationPortRange == "*" { 374 portRange.FromPort = 0 375 portRange.ToPort = 65535 376 } else { 377 portRange, err = corenetwork.ParsePortRange( 378 *rule.DestinationPortRange, 379 ) 380 if err != nil { 381 return nil, errors.Annotatef( 382 err, "parsing port range for security rule %q", 383 to.String(rule.Name), 384 ) 385 } 386 } 387 388 var protocols []string 389 switch rule.Protocol { 390 case network.SecurityRuleProtocolTCP: 391 protocols = []string{"tcp"} 392 case network.SecurityRuleProtocolUDP: 393 protocols = []string{"udp"} 394 default: 395 protocols = []string{"tcp", "udp"} 396 } 397 398 // Record the SourceAddressPrefix for the port range. 399 remotePrefix := to.String(rule.SourceAddressPrefix) 400 if remotePrefix == "" || remotePrefix == "*" { 401 remotePrefix = "0.0.0.0/0" 402 } 403 for _, protocol := range protocols { 404 portRange.Protocol = protocol 405 sourceCIDRs, ok := portSourceCIDRs[portRange] 406 if !ok { 407 sourceCIDRs = &[]string{} 408 portSourceCIDRs[portRange] = sourceCIDRs 409 } 410 *sourceCIDRs = append(*sourceCIDRs, remotePrefix) 411 } 412 } 413 // Combine all the port ranges and remote prefixes. 414 for portRange, sourceCIDRs := range portSourceCIDRs { 415 rule, err := jujunetwork.NewIngressRule( 416 portRange.Protocol, 417 portRange.FromPort, 418 portRange.ToPort, 419 *sourceCIDRs...) 420 if err != nil { 421 return nil, errors.Trace(err) 422 } 423 rules = append(rules, rule) 424 } 425 jujunetwork.SortIngressRules(rules) 426 return rules, nil 427 } 428 429 // deleteInstanceNetworkSecurityRules deletes network security rules in the 430 // internal network security group that correspond to the specified machine. 431 // 432 // This is expected to delete *all* security rules related to the instance, 433 // i.e. both the ones opened by OpenPorts above, and the ones opened for API 434 // access. 435 func deleteInstanceNetworkSecurityRules( 436 ctx context.ProviderCallContext, 437 resourceGroup string, id instance.Id, 438 nsgClient network.SecurityGroupsClient, 439 securityRuleClient network.SecurityRulesClient, 440 ) error { 441 sdkCtx := stdcontext.Background() 442 nsg, err := nsgClient.Get(sdkCtx, resourceGroup, internalSecurityGroupName, "") 443 if err != nil { 444 if err2, ok := err.(autorest.DetailedError); ok && err2.Response.StatusCode == http.StatusNotFound { 445 return nil 446 } 447 return errorutils.HandleCredentialError(errors.Annotate(err, "querying network security group"), ctx) 448 } 449 if nsg.SecurityRules == nil { 450 return nil 451 } 452 prefix := instanceNetworkSecurityRulePrefix(id) 453 for _, rule := range *nsg.SecurityRules { 454 ruleName := to.String(rule.Name) 455 if !strings.HasPrefix(ruleName, prefix) { 456 continue 457 } 458 future, err := securityRuleClient.Delete( 459 sdkCtx, 460 resourceGroup, 461 internalSecurityGroupName, 462 ruleName, 463 ) 464 if err != nil { 465 if !isNotFoundResponse(future.Response()) { 466 return errors.Annotatef(err, "deleting security rule %q", ruleName) 467 } 468 continue 469 } 470 err = future.WaitForCompletionRef(sdkCtx, securityRuleClient.Client) 471 if err != nil { 472 return errors.Annotatef(err, "deleting security rule %q", ruleName) 473 } 474 result, err := future.Result(securityRuleClient) 475 if err != nil && !isNotFoundResult(result) { 476 return errorutils.HandleCredentialError(errors.Annotatef(err, "deleting security rule %q", ruleName), ctx) 477 } 478 } 479 return nil 480 } 481 482 // instanceNetworkSecurityRulePrefix returns the unique prefix for network 483 // security rule names that relate to the instance with the given ID. 484 func instanceNetworkSecurityRulePrefix(id instance.Id) string { 485 return string(id) + "-" 486 } 487 488 // securityRuleName returns the security rule name for the given ingress rule, 489 // and prefix returned by instanceNetworkSecurityRulePrefix. 490 func securityRuleName(prefix string, rule jujunetwork.IngressRule) string { 491 ruleName := fmt.Sprintf("%s%s-%d", prefix, rule.Protocol, rule.FromPort) 492 if rule.FromPort != rule.ToPort { 493 ruleName += fmt.Sprintf("-%d", rule.ToPort) 494 } 495 // The rule parameter must have a single source cidr. 496 // Ensure the rule name can be a valid URL path component. 497 cidr := rule.SourceCIDRs[0] 498 if cidr != "0.0.0.0/0" && cidr != "*" { 499 cidr = strings.Replace(cidr, ".", "-", -1) 500 cidr = strings.Replace(cidr, "::", "-", -1) 501 cidr = strings.Replace(cidr, "/", "-", -1) 502 ruleName = fmt.Sprintf("%s-cidr-%s", ruleName, cidr) 503 } 504 return ruleName 505 } 506 507 // explodeIngressRules creates a slice of ingress rules, each rule in the 508 // result having a single source CIDR. The results contain a copy of each 509 // specified rule with each copy having one of the source CIDR values, 510 func explodeIngressRules(inRules jujunetwork.IngressRuleSlice) jujunetwork.IngressRuleSlice { 511 // If any rule has an empty source CIDR slice, a default 512 // source value of "*" is used. 513 var singleSourceIngressRules jujunetwork.IngressRuleSlice 514 for _, rule := range inRules { 515 sourceCIDRs := rule.SourceCIDRs 516 if len(sourceCIDRs) == 0 { 517 sourceCIDRs = []string{"*"} 518 } 519 for _, sr := range sourceCIDRs { 520 r := rule 521 r.SourceCIDRs = []string{sr} 522 singleSourceIngressRules = append(singleSourceIngressRules, r) 523 } 524 } 525 return singleSourceIngressRules 526 }