b6f6dece7289929b91c1198fc5035b6317306c86
[invirt/packages/invirt-web.git] / controls.py
1 """
2 Functions to perform remctls.
3 """
4
5 from sipb_xen_database import Machine, Disk, Type, NIC, CDROM, ctx, meta
6 import validation
7 from webcommon import CodeError, InvalidInput
8 import random
9 import subprocess
10 import sys
11 import time
12 import re
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 kinit(username = 'daemon/sipb-xen.mit.edu', keytab = '/etc/sipb-xen.keytab'):
27     """Kinit with a given username and keytab"""
28
29     p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
30                          stderr=subprocess.PIPE)
31     e = p.wait()
32     if e:
33         raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
34
35 def checkKinit():
36     """If we lack tickets, kinit."""
37     p = subprocess.Popen(['klist', '-s'])
38     if p.wait():
39         kinit()
40
41 def remctl(*args, **kws):
42     """Perform a remctl and return the output.
43
44     kinits if necessary, and outputs errors to stderr.
45     """
46     checkKinit()
47     p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
48                          + list(args),
49                          stdout=subprocess.PIPE,
50                          stderr=subprocess.PIPE)
51     v = p.wait()
52     if kws.get('err'):
53         return p.stdout.read(), p.stderr.read()
54     if v:
55         print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
56         print >> sys.stderr, p.stderr.read()
57         raise CodeError('ERROR on remctl')
58     return p.stdout.read()
59
60 def lvcreate(machine, disk):
61     """Create a single disk for a machine"""
62     remctl('web', 'lvcreate', machine.name,
63            disk.guest_device_name, str(disk.size))
64     
65 def makeDisks(machine):
66     """Update the lvm partitions to add a disk."""
67     for disk in machine.disks:
68         lvcreate(machine, disk)
69
70 def bootMachine(machine, cdtype):
71     """Boot a machine with a given boot CD.
72
73     If cdtype is None, give no boot cd.  Otherwise, it is the string
74     id of the CD (e.g. 'gutsy_i386')
75     """
76     if cdtype is not None:
77         out, err = remctl('control', machine.name, 'create', 
78                           cdtype, err=True)
79     else:
80         out, err = remctl('control', machine.name, 'create',
81                           err=True)
82     if 'already exists' in out:
83         raise InvalidInput('action', 'create',
84                            'VM %s is already on' % machine.name)
85     elif err:
86         raise CodeError('"%s" on "control %s create %s' 
87                         % (err, machine.name, cdtype))
88     else:
89         raise CodeError('"%s" on "control %s create %s' 
90                         % (err, machine.name, cdtype))
91
92 def registerMachine(machine):
93     """Register a machine to be controlled by the web interface"""
94     remctl('web', 'register', machine.name)
95
96 def unregisterMachine(machine):
97     """Unregister a machine to not be controlled by the web interface"""
98     remctl('web', 'unregister', machine.name)
99
100 def createVm(owner, contact, name, memory, disk_size, is_hvm, cdrom):
101     """Create a VM and put it in the database"""
102     # put stuff in the table
103     transaction = ctx.current.create_transaction()
104     try:
105         validation.validMemory(owner, memory)
106         validation.validDisk(owner, disk_size  * 1. / 1024)
107         validation.validAddVm(owner)
108         res = meta.engine.execute('select nextval('
109                                   '\'"machines_machine_id_seq"\')')
110         id = res.fetchone()[0]
111         machine = Machine()
112         machine.machine_id = id
113         machine.name = name
114         machine.memory = memory
115         machine.owner = owner
116         machine.administrator = owner
117         machine.contact = contact
118         machine.uuid = uuidToString(randomUUID())
119         machine.boot_off_cd = True
120         machine_type = Type.get_by(hvm=is_hvm)
121         machine.type_id = machine_type.type_id
122         ctx.current.save(machine)
123         disk = Disk(machine_id=machine.machine_id, 
124                     guest_device_name='hda', size=disk_size)
125         open_nics = NIC.select_by(machine_id=None)
126         if not open_nics: #No IPs left!
127             raise CodeError("No IP addresses left!  "
128                             "Contact sipb-xen-dev@mit.edu")
129         nic = open_nics[0]
130         nic.machine_id = machine.machine_id
131         nic.hostname = name
132         ctx.current.save(nic)    
133         ctx.current.save(disk)
134         transaction.commit()
135     except:
136         transaction.rollback()
137         raise
138     registerMachine(machine)
139     makeDisks(machine)
140     # tell it to boot with cdrom
141     bootMachine(machine, cdrom)
142     return machine
143
144 def getUptimes(machines=None):
145     """Return a dictionary mapping machine names to uptime strings"""
146     value_string = remctl('web', 'listvms')
147     lines = value_string.splitlines()
148     d = {}
149     for line in lines:
150         lst = line.split()
151         name, id = lst[:2]
152         uptime = ' '.join(lst[2:])
153         d[name] = uptime
154     ans = {}
155     for m in machines:
156         ans[m] = d.get(m.name)
157     return ans
158
159 def parseStatus(s):
160     """Parse a status string into nested tuples of strings.
161
162     s = output of xm list --long <machine_name>
163     """
164     values = re.split('([()])', s)
165     stack = [[]]
166     for v in values[2:-2]: #remove initial and final '()'
167         if not v:
168             continue
169         v = v.strip()
170         if v == '(':
171             stack.append([])
172         elif v == ')':
173             if len(stack[-1]) == 1:
174                 stack[-1].append('')
175             stack[-2].append(stack[-1])
176             stack.pop()
177         else:
178             if not v:
179                 continue
180             stack[-1].extend(v.split())
181     return stack[-1]
182
183 def statusInfo(machine):
184     """Return the status list for a given machine.
185
186     Gets and parses xm list --long
187     """
188     value_string, err_string = remctl('control', machine.name, 'list-long', 
189                                       err=True)
190     if 'Unknown command' in err_string:
191         raise CodeError("ERROR in remctl list-long %s is not registered" % 
192                         (machine.name,))
193     elif 'does not exist' in err_string:
194         return None
195     elif err_string:
196         raise CodeError("ERROR in remctl list-long %s:  %s" % 
197                         (machine.name, err_string))
198     status = parseStatus(value_string)
199     return status
200
201 def deleteVM(machine):
202     """Delete a VM."""
203     remctl('control', machine.name, 'destroy', err=True)
204     transaction = ctx.current.create_transaction()
205     delete_disk_pairs = [(machine.name, d.guest_device_name) 
206                          for d in machine.disks]
207     try:
208         for nic in machine.nics:
209             nic.machine_id = None
210             nic.hostname = None
211             ctx.current.save(nic)
212         for disk in machine.disks:
213             ctx.current.delete(disk)
214         for access in machine.acl:
215             ctx.current.delete(access)
216         ctx.current.delete(machine)
217         transaction.commit()
218     except:
219         transaction.rollback()
220         raise
221     for mname, dname in delete_disk_pairs:
222         remctl('web', 'lvremove', mname, dname)
223     unregisterMachine(machine)
224
225 def commandResult(user, fields):
226     start_time = 0
227     machine = validation.testMachineId(user, fields.getfirst('machine_id'))
228     action = fields.getfirst('action')
229     cdrom = fields.getfirst('cdrom')
230     if cdrom is not None and not CDROM.get(cdrom):
231         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
232     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
233                       'Delete VM'):
234         raise CodeError("Invalid action '%s'" % action)
235     if action == 'Reboot':
236         if cdrom is not None:
237             out, err = remctl('control', machine.name, 'reboot', cdrom,
238                               err=True)
239         else:
240             out, err = remctl('control', machine.name, 'reboot',
241                               err=True)
242         if err:
243             if re.match("Error: Domain '.*' does not exist.", err):
244                 raise InvalidInput("action", "reboot", 
245                                    "Machine is not on")
246             else:
247                 print >> sys.stderr, 'Error on reboot:'
248                 print >> sys.stderr, err
249                 raise CodeError('ERROR on remctl')
250                 
251     elif action == 'Power on':
252         if validation.maxMemory(user, machine) < machine.memory:
253             raise InvalidInput('action', 'Power on',
254                                "You don't have enough free RAM quota "
255                                "to turn on this machine.")
256         bootMachine(machine, cdrom)
257     elif action == 'Power off':
258         out, err = remctl('control', machine.name, 'destroy', err=True)
259         if err:
260             if re.match("Error: Domain '.*' does not exist.", err):
261                 raise InvalidInput("action", "Power off", 
262                                    "Machine is not on.")
263             else:
264                 print >> sys.stderr, 'Error on power off:'
265                 print >> sys.stderr, err
266                 raise CodeError('ERROR on remctl')
267     elif action == 'Shutdown':
268         out, err = remctl('control', machine.name, 'shutdown', err=True)
269         if err:
270             if re.match("Error: Domain '.*' does not exist.", err):
271                 raise InvalidInput("action", "Shutdown", 
272                                    "Machine is not on.")
273             else:
274                 print >> sys.stderr, 'Error on Shutdown:'
275                 print >> sys.stderr, err
276                 raise CodeError('ERROR on remctl')
277     elif action == 'Delete VM':
278         deleteVM(machine)
279
280     d = dict(user=user,
281              command=action,
282              machine=machine)
283     return d
284
285 def resizeDisk(machine_name, disk_name, new_size):
286     remctl("web", "lvresize", machine_name, disk_name, new_size)
287
288 def renameMachine(machine, old_name, new_name):
289     for disk in machine.disks:
290         remctl("web", "lvrename", old_name, 
291                disk.guest_device_name, new_name)
292     remctl("web", "moveregister", old_name, new_name)
293