github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 "fmt" 8 "net/http" 9 "strings" 10 11 "github.com/Azure/azure-sdk-for-go/arm/network" 12 "github.com/Azure/go-autorest/autorest" 13 "github.com/Azure/go-autorest/autorest/to" 14 15 "github.com/juju/errors" 16 "github.com/juju/juju/instance" 17 jujunetwork "github.com/juju/juju/network" 18 "github.com/juju/juju/status" 19 "gopkg.in/juju/names.v2" 20 ) 21 22 type azureInstance struct { 23 vmName string 24 provisioningState string 25 env *azureEnviron 26 networkInterfaces []network.Interface 27 publicIPAddresses []network.PublicIPAddress 28 } 29 30 // Id is specified in the Instance interface. 31 func (inst *azureInstance) Id() instance.Id { 32 // Note: we use Name and not Id, since all VM operations are in 33 // terms of the VM name (qualified by resource group). The ID is 34 // an internal detail. 35 return instance.Id(inst.vmName) 36 } 37 38 // Status is specified in the Instance interface. 39 func (inst *azureInstance) Status() instance.InstanceStatus { 40 instanceStatus := status.Empty 41 message := inst.provisioningState 42 switch inst.provisioningState { 43 case "Succeeded": 44 // TODO(axw) once a VM has been started, we should 45 // start using its power state to show if it's 46 // really running or not. This is just a nice to 47 // have, since we should not expect a VM to ever 48 // be stopped. 49 instanceStatus = status.Running 50 message = "" 51 case "Canceled", "Failed": 52 // TODO(axw) if the provisioning state is "Failed", then we 53 // should use the error message from the deployment description 54 // as the Message. The error details are not currently exposed 55 // in the Azure SDK. See: 56 // https://github.com/Azure/azure-sdk-for-go/issues/399 57 instanceStatus = status.ProvisioningError 58 case "Running": 59 message = "" 60 fallthrough 61 default: 62 instanceStatus = status.Provisioning 63 } 64 return instance.InstanceStatus{ 65 Status: instanceStatus, 66 Message: message, 67 } 68 } 69 70 // setInstanceAddresses queries Azure for the NICs and public IPs associated 71 // with the given set of instances. This assumes that the instances' 72 // VirtualMachines are up-to-date, and that there are no concurrent accesses 73 // to the instances. 74 func setInstanceAddresses( 75 callAPI callAPIFunc, 76 resourceGroup string, 77 nicClient network.InterfacesClient, 78 pipClient network.PublicIPAddressesClient, 79 instances []*azureInstance, 80 ) (err error) { 81 instanceNics, err := instanceNetworkInterfaces( 82 callAPI, resourceGroup, nicClient, 83 ) 84 if err != nil { 85 return errors.Annotate(err, "listing network interfaces") 86 } 87 instancePips, err := instancePublicIPAddresses( 88 callAPI, resourceGroup, pipClient, 89 ) 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 callAPI callAPIFunc, 105 resourceGroup string, 106 nicClient network.InterfacesClient, 107 ) (map[instance.Id][]network.Interface, error) { 108 var nicsResult network.InterfaceListResult 109 if err := callAPI(func() (autorest.Response, error) { 110 var err error 111 nicsResult, err = nicClient.List(resourceGroup) 112 return nicsResult.Response, err 113 }); err != nil { 114 return nil, errors.Annotate(err, "listing network interfaces") 115 } 116 if nicsResult.Value == nil || len(*nicsResult.Value) == 0 { 117 return nil, nil 118 } 119 instanceNics := make(map[instance.Id][]network.Interface) 120 for _, nic := range *nicsResult.Value { 121 instanceId := instance.Id(toTags(nic.Tags)[jujuMachineNameTag]) 122 instanceNics[instanceId] = append(instanceNics[instanceId], nic) 123 } 124 return instanceNics, nil 125 } 126 127 // interfacePublicIPAddresses lists all public IP addresses in the resource 128 // group, and returns a mapping from instance ID to the public IP addresses 129 // associated with that instance. 130 func instancePublicIPAddresses( 131 callAPI callAPIFunc, 132 resourceGroup string, 133 pipClient network.PublicIPAddressesClient, 134 ) (map[instance.Id][]network.PublicIPAddress, error) { 135 var pipsResult network.PublicIPAddressListResult 136 if err := callAPI(func() (autorest.Response, error) { 137 var err error 138 pipsResult, err = pipClient.List(resourceGroup) 139 return pipsResult.Response, err 140 }); err != nil { 141 return nil, errors.Annotate(err, "listing public IP addresses") 142 } 143 if pipsResult.Value == nil || len(*pipsResult.Value) == 0 { 144 return nil, nil 145 } 146 instancePips := make(map[instance.Id][]network.PublicIPAddress) 147 for _, pip := range *pipsResult.Value { 148 instanceId := instance.Id(toTags(pip.Tags)[jujuMachineNameTag]) 149 instancePips[instanceId] = append(instancePips[instanceId], pip) 150 } 151 return instancePips, nil 152 } 153 154 // Addresses is specified in the Instance interface. 155 func (inst *azureInstance) Addresses() ([]jujunetwork.Address, error) { 156 addresses := make([]jujunetwork.Address, 0, len(inst.networkInterfaces)+len(inst.publicIPAddresses)) 157 for _, nic := range inst.networkInterfaces { 158 if nic.Properties.IPConfigurations == nil { 159 continue 160 } 161 for _, ipConfiguration := range *nic.Properties.IPConfigurations { 162 privateIpAddress := ipConfiguration.Properties.PrivateIPAddress 163 if privateIpAddress == nil { 164 continue 165 } 166 addresses = append(addresses, jujunetwork.NewScopedAddress( 167 to.String(privateIpAddress), 168 jujunetwork.ScopeCloudLocal, 169 )) 170 } 171 } 172 for _, pip := range inst.publicIPAddresses { 173 if pip.Properties.IPAddress == nil { 174 continue 175 } 176 addresses = append(addresses, jujunetwork.NewScopedAddress( 177 to.String(pip.Properties.IPAddress), 178 jujunetwork.ScopePublic, 179 )) 180 } 181 return addresses, nil 182 } 183 184 // primaryNetworkAddress returns the instance's primary jujunetwork.Address for 185 // the internal virtual network. This address is used to identify the machine in 186 // network security rules. 187 func (inst *azureInstance) primaryNetworkAddress() (jujunetwork.Address, error) { 188 for _, nic := range inst.networkInterfaces { 189 if nic.Properties.IPConfigurations == nil { 190 continue 191 } 192 for _, ipConfiguration := range *nic.Properties.IPConfigurations { 193 if ipConfiguration.Properties.Subnet == nil { 194 continue 195 } 196 if !to.Bool(ipConfiguration.Properties.Primary) { 197 continue 198 } 199 privateIpAddress := ipConfiguration.Properties.PrivateIPAddress 200 if privateIpAddress == nil { 201 continue 202 } 203 return jujunetwork.NewScopedAddress( 204 to.String(privateIpAddress), 205 jujunetwork.ScopeCloudLocal, 206 ), nil 207 } 208 } 209 return jujunetwork.Address{}, errors.NotFoundf("internal network address") 210 } 211 212 // OpenPorts is specified in the Instance interface. 213 func (inst *azureInstance) OpenPorts(machineId string, ports []jujunetwork.PortRange) error { 214 nsgClient := network.SecurityGroupsClient{inst.env.network} 215 securityRuleClient := network.SecurityRulesClient{inst.env.network} 216 primaryNetworkAddress, err := inst.primaryNetworkAddress() 217 if err != nil { 218 return errors.Trace(err) 219 } 220 221 securityGroupName := internalSecurityGroupName 222 var nsg network.SecurityGroup 223 if err := inst.env.callAPI(func() (autorest.Response, error) { 224 var err error 225 nsg, err = nsgClient.Get(inst.env.resourceGroup, securityGroupName, "") 226 return nsg.Response, err 227 }); err != nil { 228 return errors.Annotate(err, "querying network security group") 229 } 230 231 var securityRules []network.SecurityRule 232 if nsg.Properties.SecurityRules != nil { 233 securityRules = *nsg.Properties.SecurityRules 234 } else { 235 nsg.Properties.SecurityRules = &securityRules 236 } 237 238 // Create rules one at a time; this is necessary to avoid trampling 239 // on changes made by the provisioner. We still record rules in the 240 // NSG in memory, so we can easily tell which priorities are available. 241 vmName := resourceName(names.NewMachineTag(machineId)) 242 prefix := instanceNetworkSecurityRulePrefix(instance.Id(vmName)) 243 for _, ports := range ports { 244 ruleName := securityRuleName(prefix, ports) 245 246 // Check if the rule already exists; OpenPorts must be idempotent. 247 var found bool 248 for _, rule := range securityRules { 249 if to.String(rule.Name) == ruleName { 250 found = true 251 break 252 } 253 } 254 if found { 255 logger.Debugf("security rule %q already exists", ruleName) 256 continue 257 } 258 logger.Debugf("creating security rule %q", ruleName) 259 260 priority, err := nextSecurityRulePriority(nsg, securityRuleInternalMax+1, securityRuleMax) 261 if err != nil { 262 return errors.Annotatef(err, "getting security rule priority for %s", ports) 263 } 264 265 var protocol network.SecurityRuleProtocol 266 switch ports.Protocol { 267 case "tcp": 268 protocol = network.TCP 269 case "udp": 270 protocol = network.UDP 271 default: 272 return errors.Errorf("invalid protocol %q", ports.Protocol) 273 } 274 275 var portRange string 276 if ports.FromPort != ports.ToPort { 277 portRange = fmt.Sprintf("%d-%d", ports.FromPort, ports.ToPort) 278 } else { 279 portRange = fmt.Sprint(ports.FromPort) 280 } 281 282 rule := network.SecurityRule{ 283 Properties: &network.SecurityRulePropertiesFormat{ 284 Description: to.StringPtr(ports.String()), 285 Protocol: protocol, 286 SourcePortRange: to.StringPtr("*"), 287 DestinationPortRange: to.StringPtr(portRange), 288 SourceAddressPrefix: to.StringPtr("*"), 289 DestinationAddressPrefix: to.StringPtr(primaryNetworkAddress.Value), 290 Access: network.Allow, 291 Priority: to.Int32Ptr(priority), 292 Direction: network.Inbound, 293 }, 294 } 295 if err := inst.env.callAPI(func() (autorest.Response, error) { 296 return securityRuleClient.CreateOrUpdate( 297 inst.env.resourceGroup, securityGroupName, ruleName, rule, 298 nil, // abort channel 299 ) 300 }); err != nil { 301 return errors.Annotatef(err, "creating security rule for %s", ports) 302 } 303 securityRules = append(securityRules, rule) 304 } 305 return nil 306 } 307 308 // ClosePorts is specified in the Instance interface. 309 func (inst *azureInstance) ClosePorts(machineId string, ports []jujunetwork.PortRange) error { 310 securityRuleClient := network.SecurityRulesClient{inst.env.network} 311 securityGroupName := internalSecurityGroupName 312 313 // Delete rules one at a time; this is necessary to avoid trampling 314 // on changes made by the provisioner. 315 vmName := resourceName(names.NewMachineTag(machineId)) 316 prefix := instanceNetworkSecurityRulePrefix(instance.Id(vmName)) 317 for _, ports := range ports { 318 ruleName := securityRuleName(prefix, ports) 319 logger.Debugf("deleting security rule %q", ruleName) 320 var result autorest.Response 321 if err := inst.env.callAPI(func() (autorest.Response, error) { 322 var err error 323 result, err = securityRuleClient.Delete( 324 inst.env.resourceGroup, securityGroupName, ruleName, 325 nil, // abort channel 326 ) 327 return result, err 328 }); err != nil { 329 if result.Response == nil || result.StatusCode != http.StatusNotFound { 330 return errors.Annotatef(err, "deleting security rule %q", ruleName) 331 } 332 } 333 } 334 return nil 335 } 336 337 // Ports is specified in the Instance interface. 338 func (inst *azureInstance) Ports(machineId string) (ports []jujunetwork.PortRange, err error) { 339 nsgClient := network.SecurityGroupsClient{inst.env.network} 340 securityGroupName := internalSecurityGroupName 341 var nsg network.SecurityGroup 342 if err := inst.env.callAPI(func() (autorest.Response, error) { 343 var err error 344 nsg, err = nsgClient.Get(inst.env.resourceGroup, securityGroupName, "") 345 return nsg.Response, err 346 }); err != nil { 347 return nil, errors.Annotate(err, "querying network security group") 348 } 349 if nsg.Properties.SecurityRules == nil { 350 return nil, nil 351 } 352 353 vmName := resourceName(names.NewMachineTag(machineId)) 354 prefix := instanceNetworkSecurityRulePrefix(instance.Id(vmName)) 355 for _, rule := range *nsg.Properties.SecurityRules { 356 if rule.Properties.Direction != network.Inbound { 357 continue 358 } 359 if rule.Properties.Access != network.Allow { 360 continue 361 } 362 if to.Int32(rule.Properties.Priority) <= securityRuleInternalMax { 363 continue 364 } 365 if !strings.HasPrefix(to.String(rule.Name), prefix) { 366 continue 367 } 368 369 var portRange jujunetwork.PortRange 370 if *rule.Properties.DestinationPortRange == "*" { 371 portRange.FromPort = 0 372 portRange.ToPort = 65535 373 } else { 374 portRange, err = jujunetwork.ParsePortRange( 375 *rule.Properties.DestinationPortRange, 376 ) 377 if err != nil { 378 return nil, errors.Annotatef( 379 err, "parsing port range for security rule %q", 380 to.String(rule.Name), 381 ) 382 } 383 } 384 385 var protocols []string 386 switch rule.Properties.Protocol { 387 case network.TCP: 388 protocols = []string{"tcp"} 389 case network.UDP: 390 protocols = []string{"udp"} 391 default: 392 protocols = []string{"tcp", "udp"} 393 } 394 for _, protocol := range protocols { 395 portRange.Protocol = protocol 396 ports = append(ports, portRange) 397 } 398 } 399 return ports, nil 400 } 401 402 // deleteInstanceNetworkSecurityRules deletes network security rules in the 403 // internal network security group that correspond to the specified machine. 404 // 405 // This is expected to delete *all* security rules related to the instance, 406 // i.e. both the ones opened by OpenPorts above, and the ones opened for API 407 // access. 408 func deleteInstanceNetworkSecurityRules( 409 resourceGroup string, id instance.Id, 410 nsgClient network.SecurityGroupsClient, 411 securityRuleClient network.SecurityRulesClient, 412 callAPI callAPIFunc, 413 ) error { 414 var nsg network.SecurityGroup 415 if err := callAPI(func() (autorest.Response, error) { 416 var err error 417 nsg, err = nsgClient.Get(resourceGroup, internalSecurityGroupName, "") 418 return nsg.Response, err 419 }); err != nil { 420 return errors.Annotate(err, "querying network security group") 421 } 422 if nsg.Properties.SecurityRules == nil { 423 return nil 424 } 425 prefix := instanceNetworkSecurityRulePrefix(id) 426 for _, rule := range *nsg.Properties.SecurityRules { 427 ruleName := to.String(rule.Name) 428 if !strings.HasPrefix(ruleName, prefix) { 429 continue 430 } 431 var result autorest.Response 432 err := callAPI(func() (autorest.Response, error) { 433 var err error 434 result, err = securityRuleClient.Delete( 435 resourceGroup, 436 internalSecurityGroupName, 437 ruleName, 438 nil, // abort channel 439 ) 440 return result, err 441 }) 442 if err != nil { 443 if result.Response == nil || result.StatusCode != http.StatusNotFound { 444 return errors.Annotatef(err, "deleting security rule %q", ruleName) 445 } 446 } 447 } 448 return nil 449 } 450 451 // instanceNetworkSecurityRulePrefix returns the unique prefix for network 452 // security rule names that relate to the instance with the given ID. 453 func instanceNetworkSecurityRulePrefix(id instance.Id) string { 454 return string(id) + "-" 455 } 456 457 // securityRuleName returns the security rule name for the given port range, 458 // and prefix returned by instanceNetworkSecurityRulePrefix. 459 func securityRuleName(prefix string, ports jujunetwork.PortRange) string { 460 ruleName := fmt.Sprintf("%s%s-%d", prefix, ports.Protocol, ports.FromPort) 461 if ports.FromPort != ports.ToPort { 462 ruleName += fmt.Sprintf("-%d", ports.ToPort) 463 } 464 return ruleName 465 }