github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/provider/openstack/firewaller.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package openstack 5 6 import ( 7 "fmt" 8 "regexp" 9 "strings" 10 "time" 11 12 "github.com/juju/errors" 13 "github.com/juju/retry" 14 "github.com/juju/utils/clock" 15 gooseerrors "gopkg.in/goose.v1/errors" 16 "gopkg.in/goose.v1/nova" 17 18 "github.com/juju/juju/environs" 19 "github.com/juju/juju/environs/config" 20 "github.com/juju/juju/instance" 21 "github.com/juju/juju/network" 22 ) 23 24 //factory for obtaining firawaller object. 25 type FirewallerFactory interface { 26 GetFirewaller(env environs.Environ) Firewaller 27 } 28 29 // Firewaller allows custom openstack provider behaviour. 30 // This is used in other providers that embed the openstack provider. 31 type Firewaller interface { 32 // OpenPorts opens the given port ranges for the whole environment. 33 OpenPorts(ports []network.PortRange) error 34 35 // ClosePorts closes the given port ranges for the whole environment. 36 ClosePorts(ports []network.PortRange) error 37 38 // Ports returns the port ranges opened for the whole environment. 39 Ports() ([]network.PortRange, error) 40 41 // Implementations shoud delete all global security groups. 42 DeleteGlobalGroups() error 43 44 // Implementations should return list of security groups, that belong to given instances. 45 GetSecurityGroups(ids ...instance.Id) ([]string, error) 46 47 // Implementations should set up initial security groups, if any. 48 SetUpGroups(machineId string, apiPort int) ([]nova.SecurityGroup, error) 49 50 // Set of initial networks, that should be added by default to all new instances. 51 InitialNetworks() []nova.ServerNetworks 52 53 // OpenInstancePorts opens the given port ranges for the specified instance. 54 OpenInstancePorts(inst instance.Instance, machineId string, ports []network.PortRange) error 55 56 // CloseInstancePorts closes the given port ranges for the specified instance. 57 CloseInstancePorts(inst instance.Instance, machineId string, ports []network.PortRange) error 58 59 // InstancePorts returns the port ranges opened for the specified instance. 60 InstancePorts(inst instance.Instance, machineId string) ([]network.PortRange, error) 61 } 62 63 type firewallerFactory struct { 64 } 65 66 // GetFirewaller implements FirewallerFactory 67 func (f *firewallerFactory) GetFirewaller(env environs.Environ) Firewaller { 68 return &defaultFirewaller{environ: env.(*Environ)} 69 } 70 71 type defaultFirewaller struct { 72 environ *Environ 73 } 74 75 // InitialNetworks implements Firewaller interface. 76 func (c *defaultFirewaller) InitialNetworks() []nova.ServerNetworks { 77 return []nova.ServerNetworks{} 78 } 79 80 // SetUpGroups creates the security groups for the new machine, and 81 // returns them. 82 // 83 // Instances are tagged with a group so they can be distinguished from 84 // other instances that might be running on the same OpenStack account. 85 // In addition, a specific machine security group is created for each 86 // machine, so that its firewall rules can be configured per machine. 87 // 88 // Note: ideally we'd have a better way to determine group membership so that 2 89 // people that happen to share an openstack account and name their environment 90 // "openstack" don't end up destroying each other's machines. 91 func (c *defaultFirewaller) SetUpGroups(machineId string, apiPort int) ([]nova.SecurityGroup, error) { 92 jujuGroup, err := c.setUpGlobalGroup(c.jujuGroupName(), apiPort) 93 if err != nil { 94 return nil, err 95 } 96 var machineGroup nova.SecurityGroup 97 switch c.environ.Config().FirewallMode() { 98 case config.FwInstance: 99 machineGroup, err = c.ensureGroup(c.machineGroupName(machineId), nil) 100 case config.FwGlobal: 101 machineGroup, err = c.ensureGroup(c.globalGroupName(), nil) 102 } 103 if err != nil { 104 return nil, err 105 } 106 groups := []nova.SecurityGroup{jujuGroup, machineGroup} 107 if c.environ.ecfg().useDefaultSecurityGroup() { 108 defaultGroup, err := c.environ.nova().SecurityGroupByName("default") 109 if err != nil { 110 return nil, fmt.Errorf("loading default security group: %v", err) 111 } 112 groups = append(groups, *defaultGroup) 113 } 114 return groups, nil 115 } 116 117 func (c *defaultFirewaller) setUpGlobalGroup(groupName string, apiPort int) (nova.SecurityGroup, error) { 118 return c.ensureGroup(groupName, 119 []nova.RuleInfo{ 120 { 121 IPProtocol: "tcp", 122 FromPort: 22, 123 ToPort: 22, 124 Cidr: "0.0.0.0/0", 125 }, 126 { 127 IPProtocol: "tcp", 128 FromPort: apiPort, 129 ToPort: apiPort, 130 Cidr: "0.0.0.0/0", 131 }, 132 { 133 IPProtocol: "tcp", 134 FromPort: 1, 135 ToPort: 65535, 136 }, 137 { 138 IPProtocol: "udp", 139 FromPort: 1, 140 ToPort: 65535, 141 }, 142 { 143 IPProtocol: "icmp", 144 FromPort: -1, 145 ToPort: -1, 146 }, 147 }) 148 } 149 150 // zeroGroup holds the zero security group. 151 var zeroGroup nova.SecurityGroup 152 153 // ensureGroup returns the security group with name and perms. 154 // If a group with name does not exist, one will be created. 155 // If it exists, its permissions are set to perms. 156 func (c *defaultFirewaller) ensureGroup(name string, rules []nova.RuleInfo) (nova.SecurityGroup, error) { 157 novaClient := c.environ.nova() 158 // First attempt to look up an existing group by name. 159 group, err := novaClient.SecurityGroupByName(name) 160 if err == nil { 161 // Group exists, so assume it is correctly set up and return it. 162 // TODO(jam): 2013-09-18 http://pad.lv/121795 163 // We really should verify the group is set up correctly, 164 // because deleting and re-creating environments can get us bad 165 // groups (especially if they were set up under Python) 166 return *group, nil 167 } 168 // Doesn't exist, so try and create it. 169 group, err = novaClient.CreateSecurityGroup(name, "juju group") 170 if err != nil { 171 if !gooseerrors.IsDuplicateValue(err) { 172 return zeroGroup, err 173 } else { 174 // We just tried to create a duplicate group, so load the existing group. 175 group, err = novaClient.SecurityGroupByName(name) 176 if err != nil { 177 return zeroGroup, err 178 } 179 return *group, nil 180 } 181 } 182 // The new group is created so now add the rules. 183 group.Rules = make([]nova.SecurityGroupRule, len(rules)) 184 for i, rule := range rules { 185 rule.ParentGroupId = group.Id 186 if rule.Cidr == "" { 187 // http://pad.lv/1226996 Rules that don't have a CIDR 188 // are meant to apply only to this group. If you don't 189 // supply CIDR or GroupId then openstack assumes you 190 // mean CIDR=0.0.0.0/0 191 rule.GroupId = &group.Id 192 } 193 groupRule, err := novaClient.CreateSecurityGroupRule(rule) 194 if err != nil && !gooseerrors.IsDuplicateValue(err) { 195 return zeroGroup, err 196 } 197 group.Rules[i] = *groupRule 198 } 199 return *group, nil 200 } 201 202 // GetSecurityGroups implements Firewaller interface. 203 func (c *defaultFirewaller) GetSecurityGroups(ids ...instance.Id) ([]string, error) { 204 var securityGroupNames []string 205 if c.environ.Config().FirewallMode() == config.FwInstance { 206 instances, err := c.environ.Instances(ids) 207 if err != nil { 208 return nil, err 209 } 210 securityGroupNames = make([]string, 0, len(ids)) 211 for _, inst := range instances { 212 if inst == nil { 213 continue 214 } 215 openstackName := inst.(*openstackInstance).getServerDetail().Name 216 lastDashPos := strings.LastIndex(openstackName, "-") 217 if lastDashPos == -1 { 218 return nil, fmt.Errorf("cannot identify machine ID in openstack server name %q", openstackName) 219 } 220 securityGroupName := c.machineGroupName(openstackName[lastDashPos+1:]) 221 securityGroupNames = append(securityGroupNames, securityGroupName) 222 } 223 } 224 return securityGroupNames, nil 225 } 226 227 // DeleteGlobalGroups implements Firewaller interface. 228 func (c *defaultFirewaller) DeleteGlobalGroups() error { 229 novaClient := c.environ.nova() 230 securityGroups, err := novaClient.ListSecurityGroups() 231 if err != nil { 232 return errors.Annotate(err, "cannot list security groups") 233 } 234 re, err := regexp.Compile(fmt.Sprintf("^%s(-\\d+)?$", c.jujuGroupName())) 235 if err != nil { 236 return errors.Trace(err) 237 } 238 globalGroupName := c.globalGroupName() 239 for _, group := range securityGroups { 240 if re.MatchString(group.Name) || group.Name == globalGroupName { 241 deleteSecurityGroup(novaClient, group.Name, group.Id) 242 } 243 } 244 return nil 245 } 246 247 // deleteSecurityGroup attempts to delete the security group. Should it fail, 248 // the deletion is retried due to timing issues in openstack. A security group 249 // cannot be deleted while it is in use. Theoretically we terminate all the 250 // instances before we attempt to delete the associated security groups, but 251 // in practice nova hasn't always finished with the instance before it 252 // returns, so there is a race condition where we think the instance is 253 // terminated and hence attempt to delete the security groups but nova still 254 // has it around internally. To attempt to catch this timing issue, deletion 255 // of the groups is tried multiple times. 256 func deleteSecurityGroup(novaclient *nova.Client, name, id string) { 257 logger.Debugf("deleting security group %q", name) 258 err := retry.Call(retry.CallArgs{ 259 Func: func() error { 260 return novaclient.DeleteSecurityGroup(id) 261 }, 262 NotifyFunc: func(err error, attempt int) { 263 if attempt%4 == 0 { 264 message := fmt.Sprintf("waiting to delete security group %q", name) 265 if attempt != 4 { 266 message = "still " + message 267 } 268 logger.Debugf(message) 269 } 270 }, 271 Attempts: 30, 272 Delay: time.Second, 273 Clock: clock.WallClock, 274 }) 275 if err != nil { 276 logger.Warningf("cannot delete security group %q. Used by another model?", name) 277 } 278 } 279 280 // OpenPorts implements Firewaller interface. 281 func (c *defaultFirewaller) OpenPorts(ports []network.PortRange) error { 282 if c.environ.Config().FirewallMode() != config.FwGlobal { 283 return fmt.Errorf("invalid firewall mode %q for opening ports on model", 284 c.environ.Config().FirewallMode()) 285 } 286 if err := c.openPortsInGroup(c.globalGroupName(), ports); err != nil { 287 return err 288 } 289 logger.Infof("opened ports in global group: %v", ports) 290 return nil 291 } 292 293 // ClosePorts implements Firewaller interface. 294 func (c *defaultFirewaller) ClosePorts(ports []network.PortRange) error { 295 if c.environ.Config().FirewallMode() != config.FwGlobal { 296 return fmt.Errorf("invalid firewall mode %q for closing ports on model", 297 c.environ.Config().FirewallMode()) 298 } 299 if err := c.closePortsInGroup(c.globalGroupName(), ports); err != nil { 300 return err 301 } 302 logger.Infof("closed ports in global group: %v", ports) 303 return nil 304 } 305 306 // Ports implements Firewaller interface. 307 func (c *defaultFirewaller) Ports() ([]network.PortRange, error) { 308 if c.environ.Config().FirewallMode() != config.FwGlobal { 309 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from model", 310 c.environ.Config().FirewallMode()) 311 } 312 return c.portsInGroup(c.globalGroupName()) 313 } 314 315 // OpenInstancePorts implements Firewaller interface. 316 func (c *defaultFirewaller) OpenInstancePorts(inst instance.Instance, machineId string, ports []network.PortRange) error { 317 if c.environ.Config().FirewallMode() != config.FwInstance { 318 return fmt.Errorf("invalid firewall mode %q for opening ports on instance", 319 c.environ.Config().FirewallMode()) 320 } 321 name := c.machineGroupName(machineId) 322 if err := c.openPortsInGroup(name, ports); err != nil { 323 return err 324 } 325 logger.Infof("opened ports in security group %s: %v", name, ports) 326 return nil 327 } 328 329 // CloseInstancePorts implements Firewaller interface. 330 func (c *defaultFirewaller) CloseInstancePorts(inst instance.Instance, machineId string, ports []network.PortRange) error { 331 if c.environ.Config().FirewallMode() != config.FwInstance { 332 return fmt.Errorf("invalid firewall mode %q for closing ports on instance", 333 c.environ.Config().FirewallMode()) 334 } 335 name := c.machineGroupName(machineId) 336 if err := c.closePortsInGroup(name, ports); err != nil { 337 return err 338 } 339 logger.Infof("closed ports in security group %s: %v", name, ports) 340 return nil 341 } 342 343 // InstancePorts implements Firewaller interface. 344 func (c *defaultFirewaller) InstancePorts(inst instance.Instance, machineId string) ([]network.PortRange, error) { 345 if c.environ.Config().FirewallMode() != config.FwInstance { 346 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from instance", 347 c.environ.Config().FirewallMode()) 348 } 349 name := c.machineGroupName(machineId) 350 portRanges, err := c.portsInGroup(name) 351 if err != nil { 352 return nil, err 353 } 354 return portRanges, nil 355 } 356 357 func (c *defaultFirewaller) openPortsInGroup(name string, portRanges []network.PortRange) error { 358 novaclient := c.environ.nova() 359 group, err := novaclient.SecurityGroupByName(name) 360 if err != nil { 361 return err 362 } 363 rules := portsToRuleInfo(group.Id, portRanges) 364 for _, rule := range rules { 365 _, err := novaclient.CreateSecurityGroupRule(rule) 366 if err != nil { 367 // TODO: if err is not rule already exists, raise? 368 logger.Debugf("error creating security group rule: %v", err.Error()) 369 } 370 } 371 return nil 372 } 373 374 // ruleMatchesPortRange checks if supplied nova security group rule matches the port range 375 func ruleMatchesPortRange(rule nova.SecurityGroupRule, portRange network.PortRange) bool { 376 if rule.IPProtocol == nil || rule.FromPort == nil || rule.ToPort == nil { 377 return false 378 } 379 return *rule.IPProtocol == portRange.Protocol && 380 *rule.FromPort == portRange.FromPort && 381 *rule.ToPort == portRange.ToPort 382 } 383 384 func (c *defaultFirewaller) closePortsInGroup(name string, portRanges []network.PortRange) error { 385 if len(portRanges) == 0 { 386 return nil 387 } 388 novaclient := c.environ.nova() 389 group, err := novaclient.SecurityGroupByName(name) 390 if err != nil { 391 return err 392 } 393 // TODO: Hey look ma, it's quadratic 394 for _, portRange := range portRanges { 395 for _, p := range (*group).Rules { 396 if !ruleMatchesPortRange(p, portRange) { 397 continue 398 } 399 err := novaclient.DeleteSecurityGroupRule(p.Id) 400 if err != nil { 401 return err 402 } 403 break 404 } 405 } 406 return nil 407 } 408 409 func (c *defaultFirewaller) portsInGroup(name string) (portRanges []network.PortRange, err error) { 410 group, err := c.environ.nova().SecurityGroupByName(name) 411 if err != nil { 412 return nil, err 413 } 414 for _, p := range (*group).Rules { 415 portRanges = append(portRanges, network.PortRange{ 416 Protocol: *p.IPProtocol, 417 FromPort: *p.FromPort, 418 ToPort: *p.ToPort, 419 }) 420 } 421 network.SortPortRanges(portRanges) 422 return portRanges, nil 423 } 424 func (c *defaultFirewaller) globalGroupName() string { 425 return fmt.Sprintf("%s-global", c.jujuGroupName()) 426 } 427 428 func (c *defaultFirewaller) machineGroupName(machineId string) string { 429 return fmt.Sprintf("%s-%s", c.jujuGroupName(), machineId) 430 } 431 432 func (c *defaultFirewaller) jujuGroupName() string { 433 cfg := c.environ.Config() 434 return fmt.Sprintf("juju-%v", cfg.UUID()) 435 }