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  }