1
# Copyright 2011 OpenStack LLC.
2
# Copyright 2012 Justin Santa Barbara
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6
# not use this file except in compliance with the License. You may obtain
7
# a copy of the License at
9
# http://www.apache.org/licenses/LICENSE-2.0
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
# License for the specific language governing permissions and limitations
17
"""The security groups extension."""
20
from xml.dom import minidom
25
from nova.api.openstack import common
26
from nova.api.openstack import extensions
27
from nova.api.openstack import wsgi
28
from nova.api.openstack import xmlutil
29
from nova import compute
31
from nova import exception
32
from nova import flags
33
from nova import log as logging
34
from nova import quota
35
from nova import utils
38
LOG = logging.getLogger(__name__)
40
authorize = extensions.extension_authorizer('compute', 'security_groups')
45
elem.set('parent_group_id')
47
proto = xmlutil.SubTemplateElement(elem, 'ip_protocol')
48
proto.text = 'ip_protocol'
50
from_port = xmlutil.SubTemplateElement(elem, 'from_port')
51
from_port.text = 'from_port'
53
to_port = xmlutil.SubTemplateElement(elem, 'to_port')
54
to_port.text = 'to_port'
56
group = xmlutil.SubTemplateElement(elem, 'group', selector='group')
57
name = xmlutil.SubTemplateElement(group, 'name')
59
tenant_id = xmlutil.SubTemplateElement(group, 'tenant_id')
60
tenant_id.text = 'tenant_id'
62
ip_range = xmlutil.SubTemplateElement(elem, 'ip_range',
64
cidr = xmlutil.SubTemplateElement(ip_range, 'cidr')
73
desc = xmlutil.SubTemplateElement(elem, 'description')
74
desc.text = 'description'
76
rules = xmlutil.SubTemplateElement(elem, 'rules')
77
rule = xmlutil.SubTemplateElement(rules, 'rule', selector='rules')
81
sg_nsmap = {None: wsgi.XMLNS_V11}
84
class SecurityGroupRuleTemplate(xmlutil.TemplateBuilder):
86
root = xmlutil.TemplateElement('security_group_rule',
87
selector='security_group_rule')
89
return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap)
92
class SecurityGroupTemplate(xmlutil.TemplateBuilder):
94
root = xmlutil.TemplateElement('security_group',
95
selector='security_group')
97
return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap)
100
class SecurityGroupsTemplate(xmlutil.TemplateBuilder):
102
root = xmlutil.TemplateElement('security_groups')
103
elem = xmlutil.SubTemplateElement(root, 'security_group',
104
selector='security_groups')
106
return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap)
109
class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer):
111
Deserializer to handle xml-formatted security group requests.
113
def default(self, string):
114
"""Deserialize an xml-formatted security group create request"""
115
dom = minidom.parseString(string)
117
sg_node = self.find_first_child_named(dom,
119
if sg_node is not None:
120
if sg_node.hasAttribute('name'):
121
security_group['name'] = sg_node.getAttribute('name')
122
desc_node = self.find_first_child_named(sg_node,
125
security_group['description'] = self.extract_text(desc_node)
126
return {'body': {'security_group': security_group}}
129
class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer):
131
Deserializer to handle xml-formatted security group requests.
134
def default(self, string):
135
"""Deserialize an xml-formatted security group create request"""
136
dom = minidom.parseString(string)
137
security_group_rule = self._extract_security_group_rule(dom)
138
return {'body': {'security_group_rule': security_group_rule}}
140
def _extract_security_group_rule(self, node):
141
"""Marshal the security group rule attribute of a parsed request"""
143
sg_rule_node = self.find_first_child_named(node,
144
'security_group_rule')
145
if sg_rule_node is not None:
146
ip_protocol_node = self.find_first_child_named(sg_rule_node,
148
if ip_protocol_node is not None:
149
sg_rule['ip_protocol'] = self.extract_text(ip_protocol_node)
151
from_port_node = self.find_first_child_named(sg_rule_node,
153
if from_port_node is not None:
154
sg_rule['from_port'] = self.extract_text(from_port_node)
156
to_port_node = self.find_first_child_named(sg_rule_node, "to_port")
157
if to_port_node is not None:
158
sg_rule['to_port'] = self.extract_text(to_port_node)
160
parent_group_id_node = self.find_first_child_named(sg_rule_node,
162
if parent_group_id_node is not None:
163
sg_rule['parent_group_id'] = self.extract_text(
164
parent_group_id_node)
166
group_id_node = self.find_first_child_named(sg_rule_node,
168
if group_id_node is not None:
169
sg_rule['group_id'] = self.extract_text(group_id_node)
171
cidr_node = self.find_first_child_named(sg_rule_node, "cidr")
172
if cidr_node is not None:
173
sg_rule['cidr'] = self.extract_text(cidr_node)
178
class SecurityGroupControllerBase(object):
179
"""Base class for Security Group controllers."""
182
self.compute_api = compute.API()
183
self.sgh = utils.import_object(FLAGS.security_group_handler)
185
def _format_security_group_rule(self, context, rule):
187
sg_rule['id'] = rule.id
188
sg_rule['parent_group_id'] = rule.parent_group_id
189
sg_rule['ip_protocol'] = rule.protocol
190
sg_rule['from_port'] = rule.from_port
191
sg_rule['to_port'] = rule.to_port
192
sg_rule['group'] = {}
193
sg_rule['ip_range'] = {}
195
source_group = db.security_group_get(context, rule.group_id)
196
sg_rule['group'] = {'name': source_group.name,
197
'tenant_id': source_group.project_id}
199
sg_rule['ip_range'] = {'cidr': rule.cidr}
202
def _format_security_group(self, context, group):
204
security_group['id'] = group.id
205
security_group['description'] = group.description
206
security_group['name'] = group.name
207
security_group['tenant_id'] = group.project_id
208
security_group['rules'] = []
209
for rule in group.rules:
210
security_group['rules'] += [self._format_security_group_rule(
212
return security_group
215
class SecurityGroupController(SecurityGroupControllerBase):
216
"""The Security group API controller for the OpenStack API."""
218
def _get_security_group(self, context, id):
221
security_group = db.security_group_get(context, id)
223
msg = _("Security group id should be integer")
224
raise exc.HTTPBadRequest(explanation=msg)
225
except exception.NotFound as exp:
226
raise exc.HTTPNotFound(explanation=unicode(exp))
227
return security_group
229
@wsgi.serializers(xml=SecurityGroupTemplate)
230
def show(self, req, id):
231
"""Return data about the given security group."""
232
context = req.environ['nova.context']
234
security_group = self._get_security_group(context, id)
235
return {'security_group': self._format_security_group(context,
238
def delete(self, req, id):
239
"""Delete a security group."""
240
context = req.environ['nova.context']
242
security_group = self._get_security_group(context, id)
243
if db.security_group_in_use(context, security_group.id):
244
msg = _("Security group is still in use")
245
raise exc.HTTPBadRequest(explanation=msg)
246
LOG.audit(_("Delete security group %s"), id, context=context)
247
db.security_group_destroy(context, security_group.id)
248
self.sgh.trigger_security_group_destroy_refresh(
249
context, security_group.id)
251
return webob.Response(status_int=202)
253
@wsgi.serializers(xml=SecurityGroupsTemplate)
254
def index(self, req):
255
"""Returns a list of security groups"""
256
context = req.environ['nova.context']
259
self.compute_api.ensure_default_security_group(context)
260
groups = db.security_group_get_by_project(context,
262
limited_list = common.limited(groups, req)
263
result = [self._format_security_group(context, group)
264
for group in limited_list]
266
return {'security_groups':
268
key=lambda k: (k['tenant_id'], k['name'])))}
270
@wsgi.serializers(xml=SecurityGroupTemplate)
271
@wsgi.deserializers(xml=SecurityGroupXMLDeserializer)
272
def create(self, req, body):
273
"""Creates a new security group."""
274
context = req.environ['nova.context']
277
raise exc.HTTPUnprocessableEntity()
279
security_group = body.get('security_group', None)
281
if security_group is None:
282
raise exc.HTTPUnprocessableEntity()
284
group_name = security_group.get('name', None)
285
group_description = security_group.get('description', None)
287
self._validate_security_group_property(group_name, "name")
288
self._validate_security_group_property(group_description,
290
group_name = group_name.strip()
291
group_description = group_description.strip()
293
if quota.allowed_security_groups(context, 1) < 1:
294
msg = _("Quota exceeded, too many security groups.")
295
raise exc.HTTPBadRequest(explanation=msg)
297
LOG.audit(_("Create Security Group %s"), group_name, context=context)
298
self.compute_api.ensure_default_security_group(context)
299
if db.security_group_exists(context, context.project_id, group_name):
300
msg = _('Security group %s already exists') % group_name
301
raise exc.HTTPBadRequest(explanation=msg)
303
group = {'user_id': context.user_id,
304
'project_id': context.project_id,
306
'description': group_description}
307
group_ref = db.security_group_create(context, group)
308
self.sgh.trigger_security_group_create_refresh(context, group)
310
return {'security_group': self._format_security_group(context,
313
def _validate_security_group_property(self, value, typ):
314
""" typ will be either 'name' or 'description',
315
depending on the caller
319
except AttributeError:
320
msg = _("Security group %s is not a string or unicode") % typ
321
raise exc.HTTPBadRequest(explanation=msg)
323
msg = _("Security group %s cannot be empty.") % typ
324
raise exc.HTTPBadRequest(explanation=msg)
326
msg = _("Security group %s should not be greater "
327
"than 255 characters.") % typ
328
raise exc.HTTPBadRequest(explanation=msg)
331
class SecurityGroupRulesController(SecurityGroupControllerBase):
333
@wsgi.serializers(xml=SecurityGroupRuleTemplate)
334
@wsgi.deserializers(xml=SecurityGroupRulesXMLDeserializer)
335
def create(self, req, body):
336
context = req.environ['nova.context']
340
raise exc.HTTPUnprocessableEntity()
342
if not 'security_group_rule' in body:
343
raise exc.HTTPUnprocessableEntity()
345
self.compute_api.ensure_default_security_group(context)
347
sg_rule = body['security_group_rule']
348
parent_group_id = sg_rule.get('parent_group_id', None)
350
parent_group_id = int(parent_group_id)
351
security_group = db.security_group_get(context, parent_group_id)
353
msg = _("Parent group id is not integer")
354
raise exc.HTTPBadRequest(explanation=msg)
355
except exception.NotFound as exp:
356
msg = _("Security group (%s) not found") % parent_group_id
357
raise exc.HTTPNotFound(explanation=msg)
359
msg = _("Authorize security group ingress %s")
360
LOG.audit(msg, security_group['name'], context=context)
363
values = self._rule_args_to_dict(context,
364
to_port=sg_rule.get('to_port'),
365
from_port=sg_rule.get('from_port'),
366
parent_group_id=sg_rule.get('parent_group_id'),
367
ip_protocol=sg_rule.get('ip_protocol'),
368
cidr=sg_rule.get('cidr'),
369
group_id=sg_rule.get('group_id'))
370
except Exception as exp:
371
raise exc.HTTPBadRequest(explanation=unicode(exp))
374
msg = _("Not enough parameters to build a "
376
raise exc.HTTPBadRequest(explanation=msg)
378
values['parent_group_id'] = security_group.id
380
if self._security_group_rule_exists(security_group, values):
381
msg = _('This rule already exists in group %s') % parent_group_id
382
raise exc.HTTPBadRequest(explanation=msg)
384
allowed = quota.allowed_security_group_rules(context,
388
msg = _("Quota exceeded, too many security group rules.")
389
raise exc.HTTPBadRequest(explanation=msg)
391
security_group_rule = db.security_group_rule_create(context, values)
392
self.sgh.trigger_security_group_rule_create_refresh(
393
context, [security_group_rule['id']])
394
self.compute_api.trigger_security_group_rules_refresh(context,
395
security_group_id=security_group['id'])
397
return {"security_group_rule": self._format_security_group_rule(
399
security_group_rule)}
401
def _security_group_rule_exists(self, security_group, values):
402
"""Indicates whether the specified rule values are already
403
defined in the given security group.
405
for rule in security_group.rules:
407
keys = ('group_id', 'cidr', 'from_port', 'to_port', 'protocol')
409
if rule.get(key) != values.get(key):
416
def _rule_args_to_dict(self, context, to_port=None, from_port=None,
417
parent_group_id=None, ip_protocol=None,
418
cidr=None, group_id=None):
421
if group_id is not None:
423
parent_group_id = int(parent_group_id)
424
group_id = int(group_id)
426
msg = _("Parent or group id is not integer")
427
raise exception.InvalidInput(reason=msg)
429
values['group_id'] = group_id
430
#check if groupId exists
431
db.security_group_get(context, group_id)
433
# If this fails, it throws an exception. This is what we want.
435
cidr = urllib.unquote(cidr).decode()
437
raise exception.InvalidCidr(cidr=cidr)
439
if not utils.is_valid_cidr(cidr):
440
# Raise exception for non-valid address
441
raise exception.InvalidCidr(cidr=cidr)
443
values['cidr'] = cidr
445
values['cidr'] = '0.0.0.0/0'
448
# Open everything if an explicit port range or type/code are not
449
# specified, but only if a source group was specified.
450
ip_proto_upper = ip_protocol.upper() if ip_protocol else ''
451
if (ip_proto_upper == 'ICMP' and
452
from_port is None and to_port is None):
455
elif (ip_proto_upper in ['TCP', 'UDP'] and from_port is None
456
and to_port is None):
460
if ip_protocol and from_port is not None and to_port is not None:
462
ip_protocol = str(ip_protocol)
464
from_port = int(from_port)
465
to_port = int(to_port)
467
if ip_protocol.upper() == 'ICMP':
468
raise exception.InvalidInput(reason="Type and"
469
" Code must be integers for ICMP protocol type")
471
raise exception.InvalidInput(reason="To and From ports "
474
if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']:
475
raise exception.InvalidIpProtocol(protocol=ip_protocol)
477
# Verify that from_port must always be less than
478
# or equal to to_port
479
if (ip_protocol.upper() in ['TCP', 'UDP'] and
480
from_port > to_port):
481
raise exception.InvalidPortRange(from_port=from_port,
482
to_port=to_port, msg="Former value cannot"
483
" be greater than the later")
485
# Verify valid TCP, UDP port ranges
486
if (ip_protocol.upper() in ['TCP', 'UDP'] and
487
(from_port < 1 or to_port > 65535)):
488
raise exception.InvalidPortRange(from_port=from_port,
489
to_port=to_port, msg="Valid TCP ports should"
490
" be between 1-65535")
492
# Verify ICMP type and code
493
if (ip_protocol.upper() == "ICMP" and
494
(from_port < -1 or from_port > 255 or
495
to_port < -1 or to_port > 255)):
496
raise exception.InvalidPortRange(from_port=from_port,
497
to_port=to_port, msg="For ICMP, the"
498
" type:code must be valid")
500
values['protocol'] = ip_protocol.lower()
501
values['from_port'] = from_port
502
values['to_port'] = to_port
504
# If cidr based filtering, protocol and ports are mandatory
510
def delete(self, req, id):
511
context = req.environ['nova.context']
514
self.compute_api.ensure_default_security_group(context)
517
rule = db.security_group_rule_get(context, id)
519
msg = _("Rule id is not integer")
520
raise exc.HTTPBadRequest(explanation=msg)
521
except exception.NotFound:
522
msg = _("Rule (%s) not found") % id
523
raise exc.HTTPNotFound(explanation=msg)
525
group_id = rule.parent_group_id
526
self.compute_api.ensure_default_security_group(context)
527
security_group = db.security_group_get(context, group_id)
529
msg = _("Revoke security group ingress %s")
530
LOG.audit(msg, security_group['name'], context=context)
532
db.security_group_rule_destroy(context, rule['id'])
533
self.sgh.trigger_security_group_rule_destroy_refresh(
534
context, [rule['id']])
535
self.compute_api.trigger_security_group_rules_refresh(context,
536
security_group_id=security_group['id'])
538
return webob.Response(status_int=202)
541
class ServerSecurityGroupController(SecurityGroupControllerBase):
543
@wsgi.serializers(xml=SecurityGroupsTemplate)
544
def index(self, req, server_id):
545
"""Returns a list of security groups for the given instance."""
546
context = req.environ['nova.context']
549
self.compute_api.ensure_default_security_group(context)
552
instance = self.compute_api.get(context, server_id)
553
groups = db.security_group_get_by_instance(context,
555
except exception.ApiError, e:
556
raise webob.exc.HTTPBadRequest(explanation=e.message)
557
except exception.NotAuthorized, e:
558
raise webob.exc.HTTPUnauthorized()
560
result = [self._format_security_group(context, group)
563
return {'security_groups':
565
key=lambda k: (k['tenant_id'], k['name'])))}
568
class SecurityGroupActionController(wsgi.Controller):
569
def __init__(self, *args, **kwargs):
570
super(SecurityGroupActionController, self).__init__(*args, **kwargs)
571
self.compute_api = compute.API()
572
self.sgh = utils.import_object(FLAGS.security_group_handler)
574
@wsgi.action('addSecurityGroup')
575
def _addSecurityGroup(self, req, id, body):
576
context = req.environ['nova.context']
580
body = body['addSecurityGroup']
581
group_name = body['name']
583
msg = _("Missing parameter dict")
584
raise webob.exc.HTTPBadRequest(explanation=msg)
586
msg = _("Security group not specified")
587
raise webob.exc.HTTPBadRequest(explanation=msg)
589
if not group_name or group_name.strip() == '':
590
msg = _("Security group name cannot be empty")
591
raise webob.exc.HTTPBadRequest(explanation=msg)
594
instance = self.compute_api.get(context, id)
595
self.compute_api.add_security_group(context, instance, group_name)
596
self.sgh.trigger_instance_add_security_group_refresh(
597
context, instance, group_name)
598
except exception.SecurityGroupNotFound as exp:
599
raise exc.HTTPNotFound(explanation=unicode(exp))
600
except exception.InstanceNotFound as exp:
601
raise exc.HTTPNotFound(explanation=unicode(exp))
602
except exception.Invalid as exp:
603
raise exc.HTTPBadRequest(explanation=unicode(exp))
605
return webob.Response(status_int=202)
607
@wsgi.action('removeSecurityGroup')
608
def _removeSecurityGroup(self, req, id, body):
609
context = req.environ['nova.context']
613
body = body['removeSecurityGroup']
614
group_name = body['name']
616
msg = _("Missing parameter dict")
617
raise webob.exc.HTTPBadRequest(explanation=msg)
619
msg = _("Security group not specified")
620
raise webob.exc.HTTPBadRequest(explanation=msg)
622
if not group_name or group_name.strip() == '':
623
msg = _("Security group name cannot be empty")
624
raise webob.exc.HTTPBadRequest(explanation=msg)
627
instance = self.compute_api.get(context, id)
628
self.compute_api.remove_security_group(context, instance,
630
self.sgh.trigger_instance_remove_security_group_refresh(
631
context, instance, group_name)
632
except exception.SecurityGroupNotFound as exp:
633
raise exc.HTTPNotFound(explanation=unicode(exp))
634
except exception.InstanceNotFound as exp:
635
raise exc.HTTPNotFound(explanation=unicode(exp))
636
except exception.Invalid as exp:
637
raise exc.HTTPBadRequest(explanation=unicode(exp))
639
return webob.Response(status_int=202)
642
class Security_groups(extensions.ExtensionDescriptor):
643
"""Security group support"""
645
name = "SecurityGroups"
646
alias = "security_groups"
647
namespace = "http://docs.openstack.org/compute/ext/securitygroups/api/v1.1"
648
updated = "2011-07-21T00:00:00+00:00"
650
def get_controller_extensions(self):
651
controller = SecurityGroupActionController()
652
extension = extensions.ControllerExtension(self, 'servers', controller)
655
def get_resources(self):
658
res = extensions.ResourceExtension('os-security-groups',
659
controller=SecurityGroupController())
661
resources.append(res)
663
res = extensions.ResourceExtension('os-security-group-rules',
664
controller=SecurityGroupRulesController())
665
resources.append(res)
667
res = extensions.ResourceExtension(
668
'os-security-groups',
669
controller=ServerSecurityGroupController(),
670
parent=dict(member_name='server', collection_name='servers'))
671
resources.append(res)