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 utils
37
LOG = logging.getLogger(__name__)
39
authorize = extensions.extension_authorizer('compute', 'security_groups')
44
elem.set('parent_group_id')
46
proto = xmlutil.SubTemplateElement(elem, 'ip_protocol')
47
proto.text = 'ip_protocol'
49
from_port = xmlutil.SubTemplateElement(elem, 'from_port')
50
from_port.text = 'from_port'
52
to_port = xmlutil.SubTemplateElement(elem, 'to_port')
53
to_port.text = 'to_port'
55
group = xmlutil.SubTemplateElement(elem, 'group', selector='group')
56
name = xmlutil.SubTemplateElement(group, 'name')
58
tenant_id = xmlutil.SubTemplateElement(group, 'tenant_id')
59
tenant_id.text = 'tenant_id'
61
ip_range = xmlutil.SubTemplateElement(elem, 'ip_range',
63
cidr = xmlutil.SubTemplateElement(ip_range, 'cidr')
72
desc = xmlutil.SubTemplateElement(elem, 'description')
73
desc.text = 'description'
75
rules = xmlutil.SubTemplateElement(elem, 'rules')
76
rule = xmlutil.SubTemplateElement(rules, 'rule', selector='rules')
80
sg_nsmap = {None: wsgi.XMLNS_V11}
83
class SecurityGroupRuleTemplate(xmlutil.TemplateBuilder):
85
root = xmlutil.TemplateElement('security_group_rule',
86
selector='security_group_rule')
88
return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap)
91
class SecurityGroupTemplate(xmlutil.TemplateBuilder):
93
root = xmlutil.TemplateElement('security_group',
94
selector='security_group')
96
return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap)
99
class SecurityGroupsTemplate(xmlutil.TemplateBuilder):
101
root = xmlutil.TemplateElement('security_groups')
102
elem = xmlutil.SubTemplateElement(root, 'security_group',
103
selector='security_groups')
105
return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap)
108
class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer):
110
Deserializer to handle xml-formatted security group requests.
112
def default(self, string):
113
"""Deserialize an xml-formatted security group create request"""
114
dom = minidom.parseString(string)
116
sg_node = self.find_first_child_named(dom,
118
if sg_node is not None:
119
if sg_node.hasAttribute('name'):
120
security_group['name'] = sg_node.getAttribute('name')
121
desc_node = self.find_first_child_named(sg_node,
124
security_group['description'] = self.extract_text(desc_node)
125
return {'body': {'security_group': security_group}}
128
class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer):
130
Deserializer to handle xml-formatted security group requests.
133
def default(self, string):
134
"""Deserialize an xml-formatted security group create request"""
135
dom = minidom.parseString(string)
136
security_group_rule = self._extract_security_group_rule(dom)
137
return {'body': {'security_group_rule': security_group_rule}}
139
def _extract_security_group_rule(self, node):
140
"""Marshal the security group rule attribute of a parsed request"""
142
sg_rule_node = self.find_first_child_named(node,
143
'security_group_rule')
144
if sg_rule_node is not None:
145
ip_protocol_node = self.find_first_child_named(sg_rule_node,
147
if ip_protocol_node is not None:
148
sg_rule['ip_protocol'] = self.extract_text(ip_protocol_node)
150
from_port_node = self.find_first_child_named(sg_rule_node,
152
if from_port_node is not None:
153
sg_rule['from_port'] = self.extract_text(from_port_node)
155
to_port_node = self.find_first_child_named(sg_rule_node, "to_port")
156
if to_port_node is not None:
157
sg_rule['to_port'] = self.extract_text(to_port_node)
159
parent_group_id_node = self.find_first_child_named(sg_rule_node,
161
if parent_group_id_node is not None:
162
sg_rule['parent_group_id'] = self.extract_text(
163
parent_group_id_node)
165
group_id_node = self.find_first_child_named(sg_rule_node,
167
if group_id_node is not None:
168
sg_rule['group_id'] = self.extract_text(group_id_node)
170
cidr_node = self.find_first_child_named(sg_rule_node, "cidr")
171
if cidr_node is not None:
172
sg_rule['cidr'] = self.extract_text(cidr_node)
177
class SecurityGroupControllerBase(object):
178
"""Base class for Security Group controllers."""
181
self.compute_api = compute.API()
182
self.sgh = utils.import_object(FLAGS.security_group_handler)
184
def _format_security_group_rule(self, context, rule):
186
sg_rule['id'] = rule.id
187
sg_rule['parent_group_id'] = rule.parent_group_id
188
sg_rule['ip_protocol'] = rule.protocol
189
sg_rule['from_port'] = rule.from_port
190
sg_rule['to_port'] = rule.to_port
191
sg_rule['group'] = {}
192
sg_rule['ip_range'] = {}
194
source_group = db.security_group_get(context, rule.group_id)
195
sg_rule['group'] = {'name': source_group.name,
196
'tenant_id': source_group.project_id}
198
sg_rule['ip_range'] = {'cidr': rule.cidr}
201
def _format_security_group(self, context, group):
203
security_group['id'] = group.id
204
security_group['description'] = group.description
205
security_group['name'] = group.name
206
security_group['tenant_id'] = group.project_id
207
security_group['rules'] = []
208
for rule in group.rules:
209
security_group['rules'] += [self._format_security_group_rule(
211
return security_group
214
class SecurityGroupController(SecurityGroupControllerBase):
215
"""The Security group API controller for the OpenStack API."""
217
def _get_security_group(self, context, id):
220
security_group = db.security_group_get(context, id)
222
msg = _("Security group id should be integer")
223
raise exc.HTTPBadRequest(explanation=msg)
224
except exception.NotFound as exp:
225
raise exc.HTTPNotFound(explanation=unicode(exp))
226
return security_group
228
@wsgi.serializers(xml=SecurityGroupTemplate)
229
def show(self, req, id):
230
"""Return data about the given security group."""
231
context = req.environ['nova.context']
233
security_group = self._get_security_group(context, id)
234
return {'security_group': self._format_security_group(context,
237
def delete(self, req, id):
238
"""Delete a security group."""
239
context = req.environ['nova.context']
241
security_group = self._get_security_group(context, id)
242
if db.security_group_in_use(context, security_group.id):
243
msg = _("Security group is still in use")
244
raise exc.HTTPBadRequest(explanation=msg)
245
LOG.audit(_("Delete security group %s"), id, context=context)
246
db.security_group_destroy(context, security_group.id)
247
self.sgh.trigger_security_group_destroy_refresh(
248
context, security_group.id)
250
return webob.Response(status_int=202)
252
@wsgi.serializers(xml=SecurityGroupsTemplate)
253
def index(self, req):
254
"""Returns a list of security groups"""
255
context = req.environ['nova.context']
258
self.compute_api.ensure_default_security_group(context)
259
groups = db.security_group_get_by_project(context,
261
limited_list = common.limited(groups, req)
262
result = [self._format_security_group(context, group)
263
for group in limited_list]
265
return {'security_groups':
267
key=lambda k: (k['tenant_id'], k['name'])))}
269
@wsgi.serializers(xml=SecurityGroupTemplate)
270
@wsgi.deserializers(xml=SecurityGroupXMLDeserializer)
271
def create(self, req, body):
272
"""Creates a new security group."""
273
context = req.environ['nova.context']
276
raise exc.HTTPUnprocessableEntity()
278
security_group = body.get('security_group', None)
280
if security_group is None:
281
raise exc.HTTPUnprocessableEntity()
283
group_name = security_group.get('name', None)
284
group_description = security_group.get('description', None)
286
self._validate_security_group_property(group_name, "name")
287
self._validate_security_group_property(group_description,
289
group_name = group_name.strip()
290
group_description = group_description.strip()
292
LOG.audit(_("Create Security Group %s"), group_name, context=context)
293
self.compute_api.ensure_default_security_group(context)
294
if db.security_group_exists(context, context.project_id, group_name):
295
msg = _('Security group %s already exists') % group_name
296
raise exc.HTTPBadRequest(explanation=msg)
298
group = {'user_id': context.user_id,
299
'project_id': context.project_id,
301
'description': group_description}
302
group_ref = db.security_group_create(context, group)
303
self.sgh.trigger_security_group_create_refresh(context, group)
305
return {'security_group': self._format_security_group(context,
308
def _validate_security_group_property(self, value, typ):
309
""" typ will be either 'name' or 'description',
310
depending on the caller
314
except AttributeError:
315
msg = _("Security group %s is not a string or unicode") % typ
316
raise exc.HTTPBadRequest(explanation=msg)
318
msg = _("Security group %s cannot be empty.") % typ
319
raise exc.HTTPBadRequest(explanation=msg)
321
msg = _("Security group %s should not be greater "
322
"than 255 characters.") % typ
323
raise exc.HTTPBadRequest(explanation=msg)
326
class SecurityGroupRulesController(SecurityGroupControllerBase):
328
@wsgi.serializers(xml=SecurityGroupRuleTemplate)
329
@wsgi.deserializers(xml=SecurityGroupRulesXMLDeserializer)
330
def create(self, req, body):
331
context = req.environ['nova.context']
335
raise exc.HTTPUnprocessableEntity()
337
if not 'security_group_rule' in body:
338
raise exc.HTTPUnprocessableEntity()
340
self.compute_api.ensure_default_security_group(context)
342
sg_rule = body['security_group_rule']
343
parent_group_id = sg_rule.get('parent_group_id', None)
345
parent_group_id = int(parent_group_id)
346
security_group = db.security_group_get(context, parent_group_id)
348
msg = _("Parent group id is not integer")
349
raise exc.HTTPBadRequest(explanation=msg)
350
except exception.NotFound as exp:
351
msg = _("Security group (%s) not found") % parent_group_id
352
raise exc.HTTPNotFound(explanation=msg)
354
msg = _("Authorize security group ingress %s")
355
LOG.audit(msg, security_group['name'], context=context)
358
values = self._rule_args_to_dict(context,
359
to_port=sg_rule.get('to_port'),
360
from_port=sg_rule.get('from_port'),
361
parent_group_id=sg_rule.get('parent_group_id'),
362
ip_protocol=sg_rule.get('ip_protocol'),
363
cidr=sg_rule.get('cidr'),
364
group_id=sg_rule.get('group_id'))
365
except Exception as exp:
366
raise exc.HTTPBadRequest(explanation=unicode(exp))
369
msg = _("Not enough parameters to build a "
371
raise exc.HTTPBadRequest(explanation=msg)
373
values['parent_group_id'] = security_group.id
375
if self._security_group_rule_exists(security_group, values):
376
msg = _('This rule already exists in group %s') % parent_group_id
377
raise exc.HTTPBadRequest(explanation=msg)
379
security_group_rule = db.security_group_rule_create(context, values)
380
self.sgh.trigger_security_group_rule_create_refresh(
381
context, [security_group_rule['id']])
382
self.compute_api.trigger_security_group_rules_refresh(context,
383
security_group_id=security_group['id'])
385
return {"security_group_rule": self._format_security_group_rule(
387
security_group_rule)}
389
def _security_group_rule_exists(self, security_group, values):
390
"""Indicates whether the specified rule values are already
391
defined in the given security group.
393
for rule in security_group.rules:
395
keys = ('group_id', 'cidr', 'from_port', 'to_port', 'protocol')
397
if rule.get(key) != values.get(key):
404
def _rule_args_to_dict(self, context, to_port=None, from_port=None,
405
parent_group_id=None, ip_protocol=None,
406
cidr=None, group_id=None):
409
if group_id is not None:
411
parent_group_id = int(parent_group_id)
412
group_id = int(group_id)
414
msg = _("Parent or group id is not integer")
415
raise exception.InvalidInput(reason=msg)
417
values['group_id'] = group_id
418
#check if groupId exists
419
db.security_group_get(context, group_id)
421
# If this fails, it throws an exception. This is what we want.
423
cidr = urllib.unquote(cidr).decode()
425
raise exception.InvalidCidr(cidr=cidr)
427
if not utils.is_valid_cidr(cidr):
428
# Raise exception for non-valid address
429
raise exception.InvalidCidr(cidr=cidr)
431
values['cidr'] = cidr
433
values['cidr'] = '0.0.0.0/0'
436
# Open everything if an explicit port range or type/code are not
437
# specified, but only if a source group was specified.
438
ip_proto_upper = ip_protocol.upper() if ip_protocol else ''
439
if (ip_proto_upper == 'ICMP' and
440
from_port is None and to_port is None):
443
elif (ip_proto_upper in ['TCP', 'UDP'] and from_port is None
444
and to_port is None):
448
if ip_protocol and from_port is not None and to_port is not None:
450
ip_protocol = str(ip_protocol)
452
from_port = int(from_port)
453
to_port = int(to_port)
455
if ip_protocol.upper() == 'ICMP':
456
raise exception.InvalidInput(reason="Type and"
457
" Code must be integers for ICMP protocol type")
459
raise exception.InvalidInput(reason="To and From ports "
462
if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']:
463
raise exception.InvalidIpProtocol(protocol=ip_protocol)
465
# Verify that from_port must always be less than
466
# or equal to to_port
467
if (ip_protocol.upper() in ['TCP', 'UDP'] and
468
from_port > to_port):
469
raise exception.InvalidPortRange(from_port=from_port,
470
to_port=to_port, msg="Former value cannot"
471
" be greater than the later")
473
# Verify valid TCP, UDP port ranges
474
if (ip_protocol.upper() in ['TCP', 'UDP'] and
475
(from_port < 1 or to_port > 65535)):
476
raise exception.InvalidPortRange(from_port=from_port,
477
to_port=to_port, msg="Valid TCP ports should"
478
" be between 1-65535")
480
# Verify ICMP type and code
481
if (ip_protocol.upper() == "ICMP" and
482
(from_port < -1 or from_port > 255 or
483
to_port < -1 or to_port > 255)):
484
raise exception.InvalidPortRange(from_port=from_port,
485
to_port=to_port, msg="For ICMP, the"
486
" type:code must be valid")
488
values['protocol'] = ip_protocol
489
values['from_port'] = from_port
490
values['to_port'] = to_port
492
# If cidr based filtering, protocol and ports are mandatory
498
def delete(self, req, id):
499
context = req.environ['nova.context']
502
self.compute_api.ensure_default_security_group(context)
505
rule = db.security_group_rule_get(context, id)
507
msg = _("Rule id is not integer")
508
raise exc.HTTPBadRequest(explanation=msg)
509
except exception.NotFound:
510
msg = _("Rule (%s) not found") % id
511
raise exc.HTTPNotFound(explanation=msg)
513
group_id = rule.parent_group_id
514
self.compute_api.ensure_default_security_group(context)
515
security_group = db.security_group_get(context, group_id)
517
msg = _("Revoke security group ingress %s")
518
LOG.audit(msg, security_group['name'], context=context)
520
db.security_group_rule_destroy(context, rule['id'])
521
self.sgh.trigger_security_group_rule_destroy_refresh(
522
context, [rule['id']])
523
self.compute_api.trigger_security_group_rules_refresh(context,
524
security_group_id=security_group['id'])
526
return webob.Response(status_int=202)
529
class ServerSecurityGroupController(SecurityGroupControllerBase):
531
@wsgi.serializers(xml=SecurityGroupsTemplate)
532
def index(self, req, server_id):
533
"""Returns a list of security groups for the given instance."""
534
context = req.environ['nova.context']
537
self.compute_api.ensure_default_security_group(context)
540
instance = self.compute_api.get(context, server_id)
541
groups = db.security_group_get_by_instance(context,
543
except exception.ApiError, e:
544
raise webob.exc.HTTPBadRequest(explanation=e.message)
545
except exception.NotAuthorized, e:
546
raise webob.exc.HTTPUnauthorized()
548
result = [self._format_security_group(context, group)
551
return {'security_groups':
553
key=lambda k: (k['tenant_id'], k['name'])))}
556
class SecurityGroupActionController(wsgi.Controller):
557
def __init__(self, *args, **kwargs):
558
super(SecurityGroupActionController, self).__init__(*args, **kwargs)
559
self.compute_api = compute.API()
560
self.sgh = utils.import_object(FLAGS.security_group_handler)
562
@wsgi.action('addSecurityGroup')
563
def _addSecurityGroup(self, req, id, body):
564
context = req.environ['nova.context']
568
body = body['addSecurityGroup']
569
group_name = body['name']
571
msg = _("Missing parameter dict")
572
raise webob.exc.HTTPBadRequest(explanation=msg)
574
msg = _("Security group not specified")
575
raise webob.exc.HTTPBadRequest(explanation=msg)
577
if not group_name or group_name.strip() == '':
578
msg = _("Security group name cannot be empty")
579
raise webob.exc.HTTPBadRequest(explanation=msg)
582
instance = self.compute_api.get(context, id)
583
self.compute_api.add_security_group(context, instance, group_name)
584
self.sgh.trigger_instance_add_security_group_refresh(
585
context, instance, group_name)
586
except exception.SecurityGroupNotFound as exp:
587
raise exc.HTTPNotFound(explanation=unicode(exp))
588
except exception.InstanceNotFound as exp:
589
raise exc.HTTPNotFound(explanation=unicode(exp))
590
except exception.Invalid as exp:
591
raise exc.HTTPBadRequest(explanation=unicode(exp))
593
return webob.Response(status_int=202)
595
@wsgi.action('removeSecurityGroup')
596
def _removeSecurityGroup(self, req, id, body):
597
context = req.environ['nova.context']
601
body = body['removeSecurityGroup']
602
group_name = body['name']
604
msg = _("Missing parameter dict")
605
raise webob.exc.HTTPBadRequest(explanation=msg)
607
msg = _("Security group not specified")
608
raise webob.exc.HTTPBadRequest(explanation=msg)
610
if not group_name or group_name.strip() == '':
611
msg = _("Security group name cannot be empty")
612
raise webob.exc.HTTPBadRequest(explanation=msg)
615
instance = self.compute_api.get(context, id)
616
self.compute_api.remove_security_group(context, instance,
618
self.sgh.trigger_instance_remove_security_group_refresh(
619
context, instance, group_name)
620
except exception.SecurityGroupNotFound as exp:
621
raise exc.HTTPNotFound(explanation=unicode(exp))
622
except exception.InstanceNotFound as exp:
623
raise exc.HTTPNotFound(explanation=unicode(exp))
624
except exception.Invalid as exp:
625
raise exc.HTTPBadRequest(explanation=unicode(exp))
627
return webob.Response(status_int=202)
630
class Security_groups(extensions.ExtensionDescriptor):
631
"""Security group support"""
633
name = "SecurityGroups"
634
alias = "security_groups"
635
namespace = "http://docs.openstack.org/compute/ext/securitygroups/api/v1.1"
636
updated = "2011-07-21T00:00:00+00:00"
638
def get_controller_extensions(self):
639
controller = SecurityGroupActionController()
640
extension = extensions.ControllerExtension(self, 'servers', controller)
643
def get_resources(self):
646
res = extensions.ResourceExtension('os-security-groups',
647
controller=SecurityGroupController())
649
resources.append(res)
651
res = extensions.ResourceExtension('os-security-group-rules',
652
controller=SecurityGroupRulesController())
653
resources.append(res)
655
res = extensions.ResourceExtension(
656
'os-security-groups',
657
controller=ServerSecurityGroupController(),
658
parent=dict(member_name='server', collection_name='servers'))
659
resources.append(res)