Add graphs of network usage by VMs.
[invirt/packages/invirt-web.git] / code / controls.py
1 import validation
2 from invirt.common import CodeError, InvalidInput
3 import random
4 import sys
5 import time
6 import re
7 import cache_acls
8 import yaml
9
10 from invirt.config import structs as config
11 from invirt.database import Machine, Disk, Type, NIC, CDROM, session, meta
12 from invirt.remctl import remctl as gen_remctl
13
14 # ... and stolen from xend/uuid.py
15 def randomUUID():
16     """Generate a random UUID."""
17
18     return [ random.randint(0, 255) for _ in range(0, 16) ]
19
20 def uuidToString(u):
21     """Turn a numeric UUID to a hyphen-seperated one."""
22     return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
23                      "%02x" * 6]) % tuple(u)
24 # end stolen code
25
26 def remctl(*args, **kwargs):
27     return gen_remctl(config.remote.hostname,
28                       principal='daemon/'+config.web.hostname,
29                       *args, **kwargs)
30
31 def lvcreate(machine, disk):
32     """Create a single disk for a machine"""
33     remctl('web', 'lvcreate', machine.name,
34            disk.guest_device_name, str(disk.size))
35     
36 def makeDisks(machine):
37     """Update the lvm partitions to add a disk."""
38     for disk in machine.disks:
39         lvcreate(machine, disk)
40
41 def getswap(disksize, memsize):
42     """Returns the recommended swap partition size."""
43     return int(min(disksize / 4, memsize * 1.5))
44
45 def lvinstall(machine, autoinstall):
46     disksize = machine.disks[0].size
47     memsize = machine.memory
48     swapsize = getswap(disksize, memsize)
49     imagesize = disksize - swapsize
50
51     installer_options = ['dist=%s' % autoinstall.distribution,
52                          'mirror=%s' % autoinstall.mirror,
53                          'arch=%s' % autoinstall.arch,
54                          'imagesize=%s' % imagesize]
55     if autoinstall.preseed:
56         installer_options += ['preseed=http://'+config.web.hostname+'/static/preseed/'+autoinstall.autoinstall_id+'.preseed']
57
58     remctl('control', machine.name, 'install',
59            *installer_options)
60
61 def lvcopy(machine_orig_name, machine, rootpw):
62     """Copy a golden image onto a machine's disk"""
63     remctl('web', 'lvcopy', machine_orig_name, machine.name, rootpw)
64
65 def bootMachine(machine, cdtype):
66     """Boot a machine with a given boot CD.
67
68     If cdtype is None, give no boot cd.  Otherwise, it is the string
69     id of the CD (e.g. 'gutsy_i386')
70     """
71     if cdtype is not None:
72         out, err = remctl('control', machine.name, 'create', 
73                           cdtype, err=True)
74     else:
75         out, err = remctl('control', machine.name, 'create',
76                           err=True)
77     if 'already running' in err:
78         raise InvalidInput('action', 'create',
79                            'VM %s is already on' % machine.name)
80     elif 'I need' in err and 'but dom0_min_mem is' in err:
81         raise InvalidInput('action', 'create',
82                            "We're really sorry, but our servers don't have enough capacity to create your VM right now. Try creating a VM with less RAM, or shutting down another VM of yours. Feel free to ask %s if you would like to know when we plan to have more resources." % (config.contact))
83     elif ('Booting VMs is temporarily disabled for maintenance, sorry' in err or
84           'LVM operations are temporarily disabled for maintenance, sorry' in err):
85         raise InvalidInput('action', 'create',
86                            err)
87     elif "Boot loader didn't return any data!" in err:
88         raise InvalidInput('action', 'create',
89                            "The ParaVM bootloader was unable to find an operating system to boot. Do you have GRUB configured correctly?")
90     elif 'xc_dom_find_loader: no loader found' in err:
91         raise InvalidInput('action', 'create',
92                            "The ParaVM bootloader was unable to boot the kernel you have configured. Are you sure this kernel is capable of running as a Xen ParaVM guest?")
93     elif err:
94         raise CodeError('"%s" on "control %s create %s' 
95                         % (err, machine.name, cdtype))
96
97 def createVm(username, state, owner, contact, name, description, memory, disksize, machine_type, cdrom, autoinstall):
98     """Create a VM and put it in the database"""
99     # put stuff in the table
100     session.begin()
101     try:
102         validation.Validate(username, state, name=name, description=description, owner=owner, memory=memory, disksize=disksize/1024.)
103         machine = Machine()
104         machine.name = name
105         machine.description = description
106         machine.memory = memory
107         machine.owner = owner
108         machine.administrator = None
109         machine.contact = contact
110         machine.uuid = uuidToString(randomUUID())
111         machine.boot_off_cd = True
112         machine.type = machine_type
113         session.add(machine)
114         disk = Disk(machine=machine,
115                     guest_device_name='hda', size=disksize)
116         nic = NIC.query.filter_by(machine_id=None).filter_by(reusable=True).first()
117         if not nic: #No IPs left!
118             raise CodeError("No IP addresses left!  "
119                             "Contact %s." % config.contact)
120         nic.machine = machine
121         nic.hostname = name
122         session.add(nic)
123         session.add(disk)
124         cache_acls.refreshMachine(machine)
125         makeDisks(machine)
126         session.commit()
127     except:
128         session.rollback()
129         raise
130     try:
131         if autoinstall:
132             lvinstall(machine, autoinstall)
133         else:
134             # tell it to boot with cdrom
135             bootMachine(machine, cdrom)
136     except CodeError, e:
137         deleteVM(machine)
138         raise
139     return machine
140
141 def getList():
142     """Return a dictionary mapping machine names to dicts."""
143     value_string = remctl('web', 'listvms')
144     value_dict = yaml.load(value_string, yaml.CSafeLoader)
145     return value_dict
146
147 def parseStatus(s):
148     """Parse a status string into nested tuples of strings.
149
150     s = output of xm list --long <machine_name>
151     """
152     values = re.split('([()])', s)
153     stack = [[]]
154     for v in values[2:-2]: #remove initial and final '()'
155         if not v:
156             continue
157         v = v.strip()
158         if v == '(':
159             stack.append([])
160         elif v == ')':
161             if len(stack[-1]) == 1:
162                 stack[-1].append('')
163             stack[-2].append(stack[-1])
164             stack.pop()
165         else:
166             if not v:
167                 continue
168             stack[-1].extend(v.split())
169     return stack[-1]
170
171 def statusInfo(machine):
172     """Return the status list for a given machine.
173
174     Gets and parses xm list --long
175     """
176     value_string, err_string = remctl('control', machine.name, 'list-long', 
177                                       err=True)
178     if 'Unknown command' in err_string:
179         raise CodeError("ERROR in remctl list-long %s is not registered" % 
180                         (machine.name,))
181     elif 'is not on' in err_string:
182         return None
183     elif err_string:
184         raise CodeError("ERROR in remctl list-long %s:  %s" % 
185                         (machine.name, err_string))
186     status = parseStatus(value_string)
187     return status
188
189 def listHost(machine):
190     """Return the host a machine is running on"""
191     out, err = remctl('control', machine.name, 'listhost', err=True)
192     if err:
193         return None
194     return out.strip()
195
196 def vnctoken(machine):
197     """Return a time-stamped VNC token"""
198     out, err = remctl('control', machine.name, 'vnctoken', err=True)
199     if err:
200         return None
201     return out.strip()
202
203 def deleteVM(machine):
204     """Delete a VM."""
205     remctl('control', machine.name, 'destroy', err=True)
206     session.begin()
207     delete_disk_pairs = [(machine.name, d.guest_device_name) 
208                          for d in machine.disks]
209     try:
210         for mname, dname in delete_disk_pairs:
211             remctl('web', 'lvremove', mname, dname)
212         for nic in machine.nics:
213             nic.machine_id = None
214             nic.hostname = None
215             session.add(nic)
216         for disk in machine.disks:
217             session.delete(disk)
218         session.delete(machine)
219         session.commit()
220     except:
221         session.rollback()
222         raise
223
224 def commandResult(username, state, command_name, machine_id, fields):
225     start_time = 0
226     machine = validation.Validate(username, state, machine_id=machine_id).machine
227     action = command_name
228     cdrom = fields.get('cdrom') or None
229     if cdrom is not None and not CDROM.query.filter_by(cdrom_id=cdrom).one():
230         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
231     if action not in "reboot create destroy shutdown delete".split(" "):
232         raise CodeError("Invalid action '%s'" % action)
233     if action == 'reboot':
234         if cdrom is not None:
235             out, err = remctl('control', machine.name, 'reboot', cdrom,
236                               err=True)
237         else:
238             out, err = remctl('control', machine.name, 'reboot',
239                               err=True)
240         if err:
241             if re.match("machine '.*' is not on", err):
242                 raise InvalidInput("action", "reboot", 
243                                    "Machine is not on")
244             else:
245                 print >> sys.stderr, 'Error on reboot:'
246                 print >> sys.stderr, err
247                 raise CodeError('ERROR on remctl')
248                 
249     elif action == 'create':
250         if validation.maxMemory(username, state, machine) < machine.memory:
251             raise InvalidInput('action', 'Power on',
252                                "You don't have enough free RAM quota "
253                                "to turn on this machine.")
254         bootMachine(machine, cdrom)
255     elif action == 'destroy':
256         out, err = remctl('control', machine.name, 'destroy', err=True)
257         if err:
258             if re.match("machine '.*' is not on", err):
259                 raise InvalidInput("action", "Power off", 
260                                    "Machine is not on.")
261             else:
262                 print >> sys.stderr, 'Error on power off:'
263                 print >> sys.stderr, err
264                 raise CodeError('ERROR on remctl')
265     elif action == 'shutdown':
266         out, err = remctl('control', machine.name, 'shutdown', err=True)
267         if err:
268             if re.match("machine '.*' is not on", err):
269                 raise InvalidInput("action", "Shutdown", 
270                                    "Machine is not on.")
271             else:
272                 print >> sys.stderr, 'Error on Shutdown:'
273                 print >> sys.stderr, err
274                 raise CodeError('ERROR on remctl')
275     elif action == 'delete':
276         deleteVM(machine)
277
278     d = dict(user=username,
279              command=action,
280              machine=machine)
281     return d
282
283 def resizeDisk(machine_name, disk_name, new_size):
284     remctl("web", "lvresize", machine_name, disk_name, new_size)
285
286 def renameMachine(machine, old_name, new_name):
287     for disk in machine.disks:
288         remctl("web", "lvrename", old_name, 
289                disk.guest_device_name, new_name)
290