1
"""Manage port access to machines using Nova security group extension
3
The mechanism is based on the existing scheme used by the EC2 provider.
5
Each machine is launched with two security groups, a juju group that is shared
6
across all machines and allows access to 22/tcp for ssh, and a machine group
7
just for that server so ports can be opened and closed on an individual level.
9
There is some mismatch between the port hole poking and security group models:
10
* A new security group is created for every machine
11
* Rules are not shared between service units but set up again each launch
12
* Support for port ranges is not exposed
14
The Nova security group module follows the EC2 example quite closely, but as
15
of Essex it's still under contrib and has a number of quirks:
16
* To run a server with, or add or remove groups from a server, 'name' is used
17
* To get details, delete, or add or remove rules from a group, 'id' is needed
19
The only way of getting 'id' if 'name' is known is by listing all groups then
20
looking at the details of the one with the matching name.
23
from twisted.internet import (
27
from juju import errors
29
from .client import log
32
class NovaPortManager(object):
33
"""Mapping of port-based juju interface to Nova security group actions
35
There is the potential to record some state on the instance to reduce api
36
round-trips when, for instance, launching multiple machines at once, but
40
def __init__(self, nova, environment_name):
42
self.tag = environment_name
44
def _juju_group_name(self):
45
return "juju-%s" % (self.tag,)
47
def _machine_group_name(self, machine_id):
48
return "juju-%s-%s" % (self.tag, machine_id)
50
@defer.inlineCallbacks
51
def _get_machine_group(self, machine, machine_id):
52
"""Get details of the machine specific security group
54
As only the name of the group can be derived, this means listing every
55
security group for that server and seeing which has a matching name.
57
group_name = self._machine_group_name(machine_id)
58
server_id = machine.instance_id
59
groups = yield self.nova.get_server_security_groups(server_id)
61
if group['name'] == group_name:
62
defer.returnValue(group)
63
raise errors.ProviderInteractionError(
64
"Missing security group %r for machine %r" %
65
(group_name, server_id))
67
@defer.inlineCallbacks
68
def open_port(self, machine, machine_id, port, protocol="tcp"):
69
"""Allow access to a port for the given machine only"""
70
group = yield self._get_machine_group(machine, machine_id)
71
yield self.nova.add_security_group_rule(group['id'],
72
ip_protocol=protocol, from_port=port, to_port=port)
73
log.debug("Opened %s/%s on machine %r",
74
port, protocol, machine.instance_id)
76
@defer.inlineCallbacks
77
def close_port(self, machine, machine_id, port, protocol="tcp"):
78
"""Revoke access to a port for the given machine only"""
79
group = yield self._get_machine_group(machine, machine_id)
80
for rule in group["rules"]:
81
if (port == rule["from_port"] == rule["to_port"] and
82
rule["ip_protocol"] == protocol):
83
yield self.nova.delete_security_group_rule(rule["id"])
84
log.debug("Closed %s/%s on machine %r",
85
port, protocol, machine.instance_id)
87
raise errors.ProviderInteractionError(
88
"Couldn't close unopened %s/%s on machine %r",
89
port, protocol, machine.instance_id)
91
@defer.inlineCallbacks
92
def get_opened_ports(self, machine, machine_id):
93
"""Get a set of opened port/protocol pairs for a machine"""
94
group = yield self._get_machine_group(machine, machine_id)
96
for rule in group.get("rules", []):
97
if not rule.get("group"):
98
protocol = rule["ip_protocol"]
99
from_port = rule["from_port"]
100
to_port = rule["to_port"]
101
if from_port == to_port:
102
opened_ports.add((from_port, protocol))
103
defer.returnValue(opened_ports)
105
@defer.inlineCallbacks
106
def ensure_groups(self, machine_id):
107
"""Get names of the security groups for a machine, creating if needed
109
If the juju group already exists, it is assumed to be correctly set up.
110
If the machine group already exists, it is deleted then recreated.
112
security_groups = yield self.nova.list_security_groups()
113
groups_by_name = dict((sg['name'], sg['id']) for sg in security_groups)
115
juju_group = self._juju_group_name()
116
if not juju_group in groups_by_name:
117
log.debug("Creating juju security group %s", juju_group)
118
sg = yield self.nova.create_security_group(juju_group,
119
"juju group for %s" % (self.tag,))
120
# Add external ssh access
121
yield self.nova.add_security_group_rule(sg['id'],
122
ip_protocol="tcp", from_port=22, to_port=22)
123
# Add internal group access
124
yield self.nova.add_security_group_rule(
125
parent_group_id=sg['id'], group_id=sg['id'],
126
ip_protocol="tcp", from_port=1, to_port=65535)
128
machine_group = self._machine_group_name(machine_id)
129
if machine_group in groups_by_name:
130
yield self.nova.delete_security_group(
131
groups_by_name[machine_group])
132
log.debug("Creating machine security group %s", machine_group)
133
yield self.nova.create_security_group(machine_group,
134
"juju group for %s machine %s" % (self.tag, machine_id))
136
defer.returnValue([juju_group, machine_group])
138
@defer.inlineCallbacks
139
def get_machine_groups(self, machine, with_juju_group=False):
141
ret = yield self.get_machine_groups_pure(machine, with_juju_group)
142
except errors.ProviderInteractionError, e:
143
# XXX: Need to wire up treatment of 500s properly in client
144
if getattr(e, "kind", None) == "computeError":
146
yield self.nova.get_server(machine.instance_id)
147
except errors.ProviderInteractionError, e:
148
pass # just rebinding e
149
if True or getattr(e, "kind", None) == "itemNotFound":
150
defer.returnValue(None)
152
defer.returnValue(ret)
154
@defer.inlineCallbacks
155
def get_machine_groups_pure(self, machine, with_juju_group=False):
156
server_id = machine.instance_id
157
groups = yield self.nova.get_server_security_groups(server_id)
158
juju_group = self._juju_group_name()
159
groups_by_name = dict((g['name'], g['id']) for g in groups
160
if g['name'].startswith(juju_group))
161
if juju_group not in groups_by_name:
162
# Not a juju machine, shouldn't touch
163
defer.returnValue(None)
164
if not with_juju_group:
165
groups_by_name.pop(juju_group)
166
# else assumption: only one remaining group, is the machine group
167
defer.returnValue(groups_by_name)
169
@defer.inlineCallbacks
170
def delete_juju_group(self):
171
security_groups = yield self.nova.list_security_groups()
172
juju_group = self._juju_group_name()
173
for group in security_groups:
174
if group['name'] == juju_group:
177
log.debug("Can't delete missing juju group")
179
yield self.nova.delete_security_group(group['id'])