spaces after comma
[nodemanager.git] / slivermanager.py
1 """Sliver manager.
2
3 The sliver manager has several functions.  It is responsible for
4 creating, resource limiting, starting, stopping, and destroying
5 slivers.  It provides an API for users to access these functions and
6 also to make inter-sliver resource loans.  The sliver manager is also
7 responsible for handling delegation accounts.
8 """
9
10 import string
11 import re
12 import time
13
14 import logger
15 import api, api_calls
16 import database
17 import account
18 import controller
19
20 try:
21     import sliver_lxc
22     implementation='lxc'
23     sliver_default_type='sliver.LXC'
24     sliver_class_to_register = sliver_lxc.Sliver_LXC
25     sliver_password_shell = sliver_lxc.Sliver_LXC.SHELL
26 except:
27     try:
28         import sliver_vs
29         implementation='vs'
30         sliver_default_type='sliver.VServer'
31         sliver_class_to_register = sliver_vs.Sliver_VS
32         sliver_password_shell = sliver_vs.Sliver_VS.SHELL
33     except:
34         logger.log("Could not import either sliver_lxc or sliver_vs - bailing out")
35         exit(1)
36
37 # just being safe
38 try : from plnode.bwlimit import bwmin, bwmax
39 except: bwmin, bwmax = 8, 1000*1000*1000
40
41 priority=10
42
43
44 DEFAULT_ALLOCATION = {
45     'enabled': 1,
46     # CPU parameters
47     'cpu_pct': 0, # percent CPU reserved
48     'cpu_share': 1, # proportional share
49     'cpu_cores': "0b", # reserved cpu cores <num_cores>[b]
50     'cpu_freezable': 0, # freeze processes if cpu_cores is 0
51     # bandwidth parameters
52     'net_min_rate': bwmin / 1000, # kbps
53     'net_max_rate': bwmax / 1000, # kbps
54     'net_share': 1, # proportional share
55     # bandwidth parameters over routes exempt from node bandwidth limits
56     'net_i2_min_rate': bwmin / 1000, # kbps
57     'net_i2_max_rate': bwmax / 1000, # kbps
58     'net_i2_share': 1, # proportional share
59     'net_max_kbyte' : 10546875, #Kbyte
60     'net_thresh_kbyte': 9492187, #Kbyte
61     'net_i2_max_kbyte': 31640625,
62     'net_i2_thresh_kbyte': 28476562,
63     # disk space limit
64     'disk_max': 10000000, # bytes
65     # capabilities
66     'capabilities': '',
67     # IP addresses
68     'ip_addresses': '0.0.0.0',
69
70     # NOTE: this table is further populated with resource names and
71     # default amounts via the start() function below.  This probably
72     # should be changeg and these values should be obtained via the
73     # API to myplc.
74     }
75
76 start_requested = False  # set to True in order to request that all slivers be started
77
78 # check leases and adjust the 'reservation_alive' field in slivers
79 # this is not expected to be saved as it will change for the next round
80 def adjustReservedSlivers (data):
81     """
82     On reservable nodes, tweak the 'reservation_alive' field to instruct cyclic loop
83     about what to do with slivers.
84     """
85     # only impacts reservable nodes
86     if 'reservation_policy' not in data: return
87     policy=data['reservation_policy'] 
88     if policy not in ['lease_or_idle', 'lease_or_shared']:
89         if policy is not None:
90             logger.log ("unexpected reservation_policy %(policy)s"%locals())
91         return
92
93     logger.log("slivermanager.adjustReservedSlivers")
94     now=int(time.time())
95     # scan leases that are expected to show in ascending order
96     active_lease=None
97     for lease in data['leases']:
98         if lease['t_from'] <= now and now <= lease['t_until']:
99             active_lease=lease
100             break
101
102     def is_system_sliver (sliver):
103         for d in sliver['attributes']:
104             if d['tagname']=='system' and d['value']:
105                 return True
106         return False
107
108     # mark slivers as appropriate
109     for sliver in data['slivers']:
110         # system slivers must be kept alive
111         if is_system_sliver(sliver):
112             sliver['reservation_alive']=True
113             continue
114
115         # regular slivers
116         if not active_lease:
117             # with 'idle_or_shared', just let the field out, behave like a shared node
118             # otherwise, mark all slivers as being turned down
119             if policy == 'lease_or_idle':
120                 sliver['reservation_alive']=False
121         else:
122             # there is an active lease, mark it alive and the other not
123             sliver['reservation_alive'] = sliver['name']==active_lease['name']
124
125 @database.synchronized
126 def GetSlivers(data, config = None, plc=None, fullupdate=True):
127     """This function has two purposes.  One, convert GetSlivers() data
128     into a more convenient format.  Two, even if no updates are coming
129     in, use the GetSlivers() heartbeat as a cue to scan for expired
130     slivers."""
131
132     logger.verbose("slivermanager: Entering GetSlivers with fullupdate=%r"%fullupdate)
133     for key in data.keys():
134         logger.verbose('slivermanager: GetSlivers key : ' + key)
135
136     node_id = None
137     try:
138         f = open('/etc/planetlab/node_id')
139         try: node_id = int(f.read())
140         finally: f.close()
141     except: logger.log_exc("slivermanager: GetSlivers failed to read /etc/planetlab/node_id")
142
143     if data.has_key('node_id') and data['node_id'] != node_id: return
144
145     if data.has_key('networks'):
146         for network in data['networks']:
147             if network['is_primary'] and network['bwlimit'] is not None:
148                 DEFAULT_ALLOCATION['net_max_rate'] = network['bwlimit'] / 1000
149
150     # Take initscripts (global) returned by API, build a hash scriptname->code
151     iscripts_hash = {}
152     if 'initscripts' not in data:
153         logger.log_missing_data("slivermanager.GetSlivers", 'initscripts')
154         return
155     for initscript_rec in data['initscripts']:
156         logger.verbose("slivermanager: initscript: %s" % initscript_rec['name'])
157         iscripts_hash[str(initscript_rec['name'])] = initscript_rec['script']
158
159     adjustReservedSlivers (data)
160     for sliver in data['slivers']:
161         logger.verbose("slivermanager: %s: slivermanager.GetSlivers in slivers loop"%sliver['name'])
162         rec = sliver.copy()
163         rec.setdefault('timestamp', data['timestamp'])
164
165         # convert attributes field to a proper dict
166         attributes = {}
167         for attr in rec.pop('attributes'): attributes[attr['tagname']] = attr['value']
168         rec.setdefault("attributes", attributes)
169
170         # squash keys
171         keys = rec.pop('keys')
172         rec.setdefault('keys', '\n'.join([key_struct['key'] for key_struct in keys]))
173
174         ## 'Type' isn't returned by GetSlivers() for whatever reason.  We're overloading
175         ## instantiation here, but i suppose its the same thing when you think about it. -FA
176         # Handle nm-controller here
177         if rec['instantiation'].lower() == 'nm-controller':
178             rec.setdefault('type', attributes.get('type', 'controller.Controller'))
179         else:
180             rec.setdefault('type', attributes.get('type', sliver_default_type))
181
182         # set the vserver reference.  If none, set to default.
183         rec.setdefault('vref', attributes.get('vref', 'default'))
184
185         ### set initscripts; set empty rec['initscript'] if not
186         # if tag 'initscript_code' is set, that's what we use
187         iscode = attributes.get('initscript_code', '')
188         if iscode:
189             rec['initscript']=iscode
190         else:
191             isname = attributes.get('initscript')
192             if isname is not None and isname in iscripts_hash:
193                 rec['initscript'] = iscripts_hash[isname]
194             else:
195                 rec['initscript'] = ''
196
197         # set delegations, if none, set empty
198         rec.setdefault('delegations', attributes.get("delegations", []))
199
200         # extract the implied rspec
201         rspec = {}
202         rec['rspec'] = rspec
203         for resname, default_amount in DEFAULT_ALLOCATION.iteritems():
204             try:
205                 t = type(default_amount)
206                 amount = t.__new__(t, attributes[resname])
207             except (KeyError, ValueError): amount = default_amount
208             rspec[resname] = amount
209
210         # add in sysctl attributes into the rspec
211         for key in attributes.keys():
212             if key.find("sysctl.") == 0:
213                 rspec[key] = attributes[key]
214
215         # also export tags in rspec so they make it to the sliver_vs.start call
216         rspec['tags']=attributes
217
218         database.db.deliver_record(rec)
219     if fullupdate: database.db.set_min_timestamp(data['timestamp'])
220     # slivers are created here.
221     database.db.sync()
222
223 def deliver_ticket(data):
224     return GetSlivers(data, fullupdate=False)
225
226 def start():
227     # No default allocation values for LXC yet, think if its necessary given
228     # that they are also default allocation values in this module
229     if implementation == 'vs':
230         for resname, default_amount in sliver_vs.DEFAULT_ALLOCATION.iteritems():
231             DEFAULT_ALLOCATION[resname]=default_amount
232
233     account.register_class(sliver_class_to_register)
234     account.register_class(controller.Controller)
235     database.start()
236     api_calls.deliver_ticket = deliver_ticket
237     api.start()
238
239 ### check if a sliver is running
240 ### a first step to a unified code for codemux
241 def is_running (name):
242     if implementation=='vs':
243         import vserver
244         return vserver.VServer(name).is_running()
245     else:
246         import libvirt
247         running = False
248         try:
249             conn = libvirt.open('lxc://')
250             dom  = conn.lookupByName(name)
251             running = dom.info()[0] == libvirt.VIR_DOMAIN_RUNNING
252         finally:
253             conn.close()
254         return running