1 from types import StringTypes
4 from datetime import datetime, timedelta
5 from collections import defaultdict
7 from PLC.Faults import *
8 from PLC.Parameter import Parameter, Mixed
9 from PLC.Debug import profile
10 from PLC.Roles import Role, Roles
11 from PLC.Nodes import Node
12 from PLC.Persons import Person, Persons
13 from PLC.SlicePersons import SlicePerson, SlicePersons
14 from PLC.SliceNodes import SliceNode, SliceNodes
15 from PLC.SliceInstances import SliceInstance, SliceInstances
16 from PLC.SliceTags import SliceTag, SliceTags
17 from PLC.Timestamp import Timestamp
18 from PLC.Storage.AlchemyObject import AlchemyObj
20 class Slice(AlchemyObj):
22 Representation of a row in the slices table. To use, optionally
23 instantiate with a dict of values. Update as you would a
24 dict. Commit to the database with sync().To use, instantiate
25 with a dict of values.
31 'slice_id': Parameter(int, "Slice identifier", primary_key=True),
32 'site_id': Parameter(int, "Identifier of the site to which this slice belongs"),
33 'tenant_id': Parameter(int, "Keystone tenant identifier"),
34 'name': Parameter(str, "Slice name", max = 32),
35 'instantiation': Parameter(str, "Slice instantiation state", nullok=True),
36 'url': Parameter(str, "URL further describing this slice", max = 254, nullok = True),
37 'description': Parameter(str, "Slice description", max = 2048, nullok = True),
38 'max_nodes': Parameter(int, "Maximum number of nodes that can be assigned to this slice", default=100),
39 'creator_person_id': Parameter(str, "Identifier of the account that created this slice"),
40 'created': Parameter(datetime, "Date and time when slice was created, in seconds since UNIX epoch", ro = True),
41 'expires': Parameter(datetime, "Date and time when slice expires, in seconds since UNIX epoch"),
42 'node_ids': Parameter([str], "List of nodes in this slice", joined = True),
43 'instance_ids': Parameter([str], "List of instances running under this slice", joined = True),
44 'person_ids': Parameter([str], "List of accounts that can use this slice", joined = True),
45 'slice_tag_ids': Parameter([int], "List of slice attributes", joined = True),
46 'peer_id': Parameter(int, "Peer to which this slice belongs", nullok = True),
47 'peer_slice_id': Parameter(int, "Foreign slice identifier at peer", nullok = True),
51 def validate_name(self, name):
52 # N.B.: Responsibility of the caller to ensure that login_base
53 # portion of the slice name corresponds to a valid site, if
57 # 2. Begins with login_base (letters or numbers).
58 # 3. Then single underscore after login_base.
59 # 4. Then letters, numbers, or underscores.
60 good_name = r'^[a-z0-9]+_[a-zA-Z0-9_]+$'
62 not re.match(good_name, name):
63 raise PLCInvalidArgument, "Invalid slice name"
65 conflicts = Slices(self.api, [name])
66 for slice in conflicts:
67 if 'slice_id' not in self or self['slice_id'] != slice['slice_id']:
68 raise PLCInvalidArgument, "Slice name already in use, %s"%name
72 def validate_expires(self, expires):
73 # N.B.: Responsibility of the caller to ensure that expires is
74 # not too far into the future.
75 check_future = not ('is_deleted' in self and self['is_deleted'])
76 return Timestamp.sql_validate( expires, check_future = check_future)
78 def add_person(self, person_filter, role_name=None):
79 assert 'slice_id' in self
80 assert 'tenant_id' in self
83 roles = Roles(self.api, role_name)
85 raise PLCInvalidArgument, "No such role %s" % role_name
87 tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
88 persons = Persons(self.api, person_filter)
89 for person in persons:
90 keystone_user = self.api.client_shell.keystone.users.find(id=person['keystone_id'])
91 tenant.add_user(keystone_user, role.object)
92 slice_person = SlicePerson(self.api, {'slice_id': self['slice_id'],
93 'person_id': person['person_id']})
96 def remove_person(self, person_filter, role=None):
97 assert 'slice_id' in self
98 assert 'tenant_id' in self
101 roles = Roles(self.api, role_name)
103 raise PLCInvalidArgument, "No such role %s" % role_name
105 tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
106 persons = Persons(self.api, person_filter)
107 for person in persons:
108 keystone_user = self.api.client_shell.keystone.users.find(id=person['keystone_id'])
109 tenant.remove_user(keystone_user, role.object)
110 slice_person = SlicePerson(self.api, {'slice_id': self['slice_id'],
111 'person_id': person['person_id']})
112 slice_person.delete()
115 def add_node(self, node_filter, commit=True):
116 from PLC.Nodes import Nodes
117 assert 'slice_id' in self
118 nodes = Nodes(self.api, node_filter)
120 slice_node = SliceNode(self.api, {'slice_id': self['slice_id'],
121 'node_id': node['node_id']})
124 def remove_node(self, node_filter, commit=True):
125 from PLC.Nodes import Nodes
126 assert 'slice_id' in self
127 nodes = Nodes(self.api, node_filter)
129 slice_node = SliceNode(self.api, {'slice_id': self['slice_id'],
130 'node_id': node['node_id']})
134 def spawn_instances(self, nodes):
135 # use the caller's nova keypair
136 keypairs = self.api.client_shell.nova.keypairs.list()
138 raise PLCInvalidArgument("caller has no nova key")
139 key_name = keypairs[0].name
141 # get all public keys
142 from PLC.Persons import Persons
143 from PLC.Keys import Keys
144 persons = Persons(self.api, self['person_ids'])
146 for person in persons:
147 key_ids.extend(person['key_ids'])
148 keys = Keys(self.api, key_ids)
149 pubkeys = [k['key'] for k in keys]
150 authorized_keys = "\n".join(pubkeys)
151 files = {'/root/.ssh/authorized_keys': authorized_keys}
153 # sort slice tags by node (sliver)
154 slice_tags = SliceTags(self.api, {'slice_id': self['slice_id']})
155 sliver_tags = defaultdict(dict)
156 for slice_tag in slice_tags:
157 node_id = slice_tag['node_id']
158 sliver_tags[node_id][slice_tag['tagname']] = slice_tag
161 image = self.api.config.nova_default_image
162 slice_image = sliver_tags[None].get('image')
163 sliver_image = sliver_tags[node['node_id']].get('image')
164 if sliver_image is not None: # sliver tag
165 image = sliver_image.get('value')
166 elif slice_image is not None: # sliver tag
167 image = slice_image.get('value')
170 def get_flavor(node):
171 flavor = self.api.config.nova_default_flavor
172 slice_flavor = sliver_tags[None].get('flavor')
173 sliver_flavor = sliver_tags[node['node_id']].get('flavor')
174 if sliver_flavor is not None: # sliver tag
175 flavor = sliver_flavor.get('value')
176 elif slice_flavor is not None: # slice tag
177 flavor = slice_flavor.get('value')
181 def get_security_group(node):
182 security_group = self.api.config.nova_default_security_group
183 slice_security_group = sliver_tags[None].get('security_group')
184 sliver_security_group = sliver_tags[node['node_id']].get('security_group')
185 if sliver_security_group is not None: # sliver tag
186 security_group = slice_security_group.get('value')
187 elif slice_security_group is not None: # slice tag
188 security_group = slice_security_group.get('value')
189 return security_group
192 if self['slice_id'] not in node['slice_ids']:
193 image = get_image(node)
194 flavor = get_flavor(node)
195 security_group = get_security_group(node)
196 flavor_id = self.api.client_shell.nova.flavors.find(name=flavor)
197 images = self.api.client_shell.glance.get_images(name=image)
199 raise PLCInvalidArgument('Image bot found')
200 image_id = images[0]['id']
201 hints = {'force_hosts': node['hostname']}
202 server = self.api.client_shell.nova.servers.create(
207 security_group = security_group,
209 scheduler_hints=hints)
210 slice_instance = SliceInstance(self.api, {'slice_id': self['slice_id'],
211 'instance_id': server.id})
212 slice_instance.sync()
214 def destroy_instances(self, nodes):
215 hostnames = [n['hostname'] for n in nodes]
216 servers = self.api.client_shell.nova.servers.list()
217 for server in servers:
219 hostname = server._info['OS-EXT-SRV-ATTR:host']
220 if self['name'] == name and hostname in hostnames:
221 instance_id = server.id
222 self.api.client_shell.nova.servers.delete(server)
223 AlchemyObj.delete(SliceInstance, filter={'slice_id': self['slice_id'],
224 'instance_id': instance_id})
227 def create_network(self):
228 self.api.client_shell.quantum.create_network(name=self['name'],
229 admin_state_up=False)
230 def delete_network(self):
231 nets = self.api.client_shell.quantum.list_networks(name=self['name'],
232 tenant_id=self['tenant_id'])['networks']
234 # delete all subnets:
235 #subnets = self.api.client_shell.quantum.list_subnets(network_id=net['network_id'])['subnets']
236 for subnet_id in net['subnets']:
237 self.delete_subnet(subnet_id)
238 self.api.client_shell.quantum.delete_network(net['id'])
241 def create_subnet(self, cidr_ip, ip_version, start, end):
242 nets = self.api.client_shell.quantum.list_networks(name=self['name'],
243 tenant_id=self['tenant_id'])['networks']
244 # cannot create a subnet if there is no network
248 allocation_pools = [{'start': start, 'end': end}]
249 self.api.client_shell.quantum.create_subnet(network_id=net['id'],
250 ip_version=ip_version,
252 allocation_pools=allocation_pools)
254 def delete_subnet(self, id=None):
256 self.api.client_shell.quantum.delete_subnet(id=id)
259 subnets = self.api.client_shell.quantum.list_subnets(name=self['name'],
260 tenant_id=self['tenant_id'])['subnets']
261 for subnet in subnets:
262 self.api.client_shell.quantum.delete_subnet(id=id)
265 def process_tags(self):
266 # create a subnet for each subnet tag if one doesn't alredy exist
267 tags = SliceTags(self.api, filter={'slice_id': self['slice_id']})
273 if tag['tagname'] == 'subnet_cidr':
274 subnet_cidr = tag['value']
275 elif tag['tagname'] == 'subnet_start':
276 subnet_start = tag['value']
277 elif tag['tagname'] == 'subnet_end':
278 subnet_end = tag['value']
280 if subnet_cidr and subnet_start and subnet_end:
281 allocation_pools = [{'start': subnet_start, 'end': subnet_end}]
282 subnets = self.api.client_shell.quantum.list_subnets(name=self['name'],
283 tenant_id=self['tenant_id'],
285 allocation_pools=allocation_pools)
287 #add_to_node_whitelist = Row.add_object(Node, 'node_slice_whitelist')
288 #delete_from_node_whitelist = Row.remove_object(Node, 'node_slice_whitelist')
290 def sync(self, commit = True, validate=True):
292 Add or update a slice.
294 # sync the nova record and the plc record
295 AlchemyObj.sync(self, commit=commit, validate=validate)
296 # create the nova record
297 nova_fields = ['enabled', 'description']
298 nova_can_update = lambda (field, value): field in nova_fields
299 nova_slice = dict(filter(nova_can_update, self.items()))
300 nova_slice['tenant_name'] = self['name']
301 if 'slice_id' not in self:
303 # Before a new slice is added, delete expired slices
304 #expired = Slices(self.api, expires = -int(time.time()))
305 #for slice in expired:
306 # slice.delete(commit)
307 self.object = self.api.client_shell.keystone.tenants.create(**nova_slice)
308 self['tenant_id'] = self.object.id
309 self['created'] = now
310 self['expires'] = now + timedelta(days=14)
311 AlchemyObj.insert(self, dict(self))
312 slice = AlchemyObj.select(self, filter={'tenant_id': self['tenant_id']})[0]
313 self['slice_id'] = slice.slice_id
315 # create quantum network
316 self.create_network()
318 self.object = self.api.client_shell.keystone.tenants.update(self['tenant_id'], **nova_slice)
319 AlchemyObj.updatedb(self, {'slice_id': self['slice_id']}, dict(self))
321 def delete(self, commit = True):
323 Delete existing slice.
325 assert 'slice_id' in self
326 assert 'tenant_id' in self
328 # delete quantum networks
329 self.delete_network()
331 # delete the nova object
332 tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
333 self.api.client_shell.keystone.tenants.delete(tenant)
335 # delete relationships
336 for slice_person in SlicePerson().select(filter={'slice_id': self['slice_id']}):
337 slice_person.delete()
338 for slice_node in SliceNode().select(filter={'slice_id': self['slice_id']}):
340 for slice_instance in SliceInstance().select(filter={'slice_id': self['slice_id']}):
341 slice_instance.delete()
342 for slice_tag in SliceTag().select(filter={'slice_id': self['slice_id']}):
346 AlchemyObj.delete(self, filter={'slice_id': self['slice_id']})
350 Representation of row(s) from the slices table in the
354 def __init__(self, api, slice_filter = None, columns = None, expires = int(time.time())):
356 # the view that we're selecting upon: start with view_slices
358 slices = Slice().select()
359 elif isinstance (slice_filter, StringTypes):
360 slices = Slice().select(filter={'name': slice_filter})
361 elif isinstance(slice_filter, dict):
362 slices = Slice().select(filter=slice_filter)
363 elif isinstance(slice_filter, (list, tuple, set)):
364 slices = Slice().select()
365 slices = [slice for slice in slices if slice.slice_id in slice_filter or slice.name in slice_filter]
367 raise PLCInvalidArgument, "Wrong slice filter %r"%slice_filter
370 slice = Slice(api, object=slice)
371 if not columns or 'person_ids' in columns:
372 slice_persons = SlicePerson().select(filter={'slice_id': slice['slice_id']})
373 slice['person_ids'] = [rec.person_id for rec in slice_persons]
375 # we need to get the instance ids if node_ids is specified
376 if not columns or 'instance_ids' in columns or 'node_ids' in columns:
377 slice_instances = SliceInstance().select(filter={'slice_id': slice['slice_id']})
378 slice['instance_ids'] = [rec.instance_id for rec in slice_instances]
379 if not columns or 'node_ids' in columns:
380 #slice_nodes = SliceNode().select(filter={'slice_id': slice['slice_id']})
381 #slice['node_ids'] = [rec.node_id for rec in slice_nodes]
382 # need to look up the manually look up each instance's host and query plc
384 instances = api.client_shell.nova.servers.list()
385 hostnames = [s._info['OS-EXT-SRV-ATTR:host'] for s in instances \
386 if s.id in slice['instance_ids']]
387 nodes = Node().select(filter={'hostname': hostnames})
388 slice['node_ids'] = [rec.node_id for rec in nodes]
390 if not columns or 'slice_tag_ids' in columns:
391 slice_tags = SliceTag().select(filter={'slice_id': slice['slice_id']})
392 slice['slice_tag_ids'] = [rec.slice_tag_id for rec in slice_tags]
396 def refresh(self, api):
398 Import tenants from keystone.
401 slices = Slice().select()
402 slice_names = [slice.name for slice in slices]
404 # get current tenants
405 tenants = api.client_shell.keystone.tenants.list()
407 # add tenants that dont already exist
408 for tenant in tenants:
409 # site tenants should not contain '_'
410 if '_' in tenant.name and tenant.name not in slice_names:
411 description = tenant.description
412 if not description: description = tenant.name
413 slice = Slice(api, {'name': tenant.name,
414 'tenant_id': tenant.id,
415 'enabled': tenant.enabled,
416 'description': description,
421 # slice may have a login base prefix that doesn't exist yet.