Another oversight in the web remctl code
[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     ip = machine.nics[0].ip
51     remctl('control', machine.name, 'install', 
52            'dist=%s' % autoinstall.distribution,
53            'mirror=%s' % autoinstall.mirror,
54            'imagesize=%s' % imagesize)
55
56 def lvcopy(machine_orig_name, machine, rootpw):
57     """Copy a golden image onto a machine's disk"""
58     remctl('web', 'lvcopy', machine_orig_name, machine.name, rootpw)
59
60 def bootMachine(machine, cdtype):
61     """Boot a machine with a given boot CD.
62
63     If cdtype is None, give no boot cd.  Otherwise, it is the string
64     id of the CD (e.g. 'gutsy_i386')
65     """
66     if cdtype is not None:
67         out, err = remctl('control', machine.name, 'create', 
68                           cdtype, err=True)
69     else:
70         out, err = remctl('control', machine.name, 'create',
71                           err=True)
72     if 'already running' in err:
73         raise InvalidInput('action', 'create',
74                            'VM %s is already on' % machine.name)
75     elif err:
76         raise CodeError('"%s" on "control %s create %s' 
77                         % (err, machine.name, cdtype))
78
79 def createVm(username, state, owner, contact, name, description, memory, disksize, machine_type, cdrom, autoinstall):
80     """Create a VM and put it in the database"""
81     # put stuff in the table
82     session.begin()
83     try:
84         validation.Validate(username, state, name=name, description=description, owner=owner, memory=memory, disksize=disksize/1024.)
85         machine = Machine()
86         machine.name = name
87         machine.description = description
88         machine.memory = memory
89         machine.owner = owner
90         machine.administrator = owner
91         machine.contact = contact
92         machine.uuid = uuidToString(randomUUID())
93         machine.boot_off_cd = True
94         machine.type = machine_type
95         session.save_or_update(machine)
96         disk = Disk(machine=machine,
97                     guest_device_name='hda', size=disksize)
98         nic = NIC.query().filter_by(machine_id=None).first()
99         if not nic: #No IPs left!
100             raise CodeError("No IP addresses left!  "
101                             "Contact %s." % config.web.errormail)
102         nic.machine = machine
103         nic.hostname = name
104         session.save_or_update(nic)
105         session.save_or_update(disk)
106         cache_acls.refreshMachine(machine)
107         session.commit()
108     except:
109         session.rollback()
110         raise
111     makeDisks(machine)
112     if autoinstall:
113         lvinstall(machine, autoinstall)
114     else:
115         # tell it to boot with cdrom
116         bootMachine(machine, cdrom)
117     return machine
118
119 def getList():
120     """Return a dictionary mapping machine names to dicts."""
121     value_string = remctl('web', 'listvms')
122     value_dict = yaml.load(value_string, yaml.CSafeLoader)
123     return value_dict
124
125 def parseStatus(s):
126     """Parse a status string into nested tuples of strings.
127
128     s = output of xm list --long <machine_name>
129     """
130     values = re.split('([()])', s)
131     stack = [[]]
132     for v in values[2:-2]: #remove initial and final '()'
133         if not v:
134             continue
135         v = v.strip()
136         if v == '(':
137             stack.append([])
138         elif v == ')':
139             if len(stack[-1]) == 1:
140                 stack[-1].append('')
141             stack[-2].append(stack[-1])
142             stack.pop()
143         else:
144             if not v:
145                 continue
146             stack[-1].extend(v.split())
147     return stack[-1]
148
149 def statusInfo(machine):
150     """Return the status list for a given machine.
151
152     Gets and parses xm list --long
153     """
154     value_string, err_string = remctl('control', machine.name, 'list-long', 
155                                       err=True)
156     if 'Unknown command' in err_string:
157         raise CodeError("ERROR in remctl list-long %s is not registered" % 
158                         (machine.name,))
159     elif 'is not on' in err_string:
160         return None
161     elif err_string:
162         raise CodeError("ERROR in remctl list-long %s:  %s" % 
163                         (machine.name, err_string))
164     status = parseStatus(value_string)
165     return status
166
167 def listHost(machine):
168     """Return the host a machine is running on"""
169     out, err = remctl('control', machine.name, 'listhost', err=True)
170     if err:
171         return None
172     return out.strip()
173
174 def vnctoken(machine):
175     """Return a time-stamped VNC token"""
176     out, err = remctl('control', machine.name, 'vnctoken', err=True)
177     if err:
178         return None
179     return out.strip()
180
181 def deleteVM(machine):
182     """Delete a VM."""
183     remctl('control', machine.name, 'destroy', err=True)
184     session.begin()
185     delete_disk_pairs = [(machine.name, d.guest_device_name) 
186                          for d in machine.disks]
187     try:
188         for mname, dname in delete_disk_pairs:
189             remctl('web', 'lvremove', mname, dname)
190         for nic in machine.nics:
191             nic.machine_id = None
192             nic.hostname = None
193             session.save_or_update(nic)
194         for disk in machine.disks:
195             session.delete(disk)
196         session.delete(machine)
197         session.commit()
198     except:
199         session.rollback()
200         raise
201
202 def commandResult(username, state, fields):
203     start_time = 0
204     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
205     action = fields.getfirst('action')
206     cdrom = fields.getfirst('cdrom')
207     if cdrom is not None and not CDROM.query().filter_by(cdrom_id=cdrom).one():
208         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
209     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
210                       'Delete VM'):
211         raise CodeError("Invalid action '%s'" % action)
212     if action == 'Reboot':
213         if cdrom is not None:
214             out, err = remctl('control', machine.name, 'reboot', cdrom,
215                               err=True)
216         else:
217             out, err = remctl('control', machine.name, 'reboot',
218                               err=True)
219         if err:
220             if re.match("machine '.*' is not on", err):
221                 raise InvalidInput("action", "reboot", 
222                                    "Machine is not on")
223             else:
224                 print >> sys.stderr, 'Error on reboot:'
225                 print >> sys.stderr, err
226                 raise CodeError('ERROR on remctl')
227                 
228     elif action == 'Power on':
229         if validation.maxMemory(username, state, machine) < machine.memory:
230             raise InvalidInput('action', 'Power on',
231                                "You don't have enough free RAM quota "
232                                "to turn on this machine.")
233         bootMachine(machine, cdrom)
234     elif action == 'Power off':
235         out, err = remctl('control', machine.name, 'destroy', err=True)
236         if err:
237             if re.match("machine '.*' is not on", err):
238                 raise InvalidInput("action", "Power off", 
239                                    "Machine is not on.")
240             else:
241                 print >> sys.stderr, 'Error on power off:'
242                 print >> sys.stderr, err
243                 raise CodeError('ERROR on remctl')
244     elif action == 'Shutdown':
245         out, err = remctl('control', machine.name, 'shutdown', err=True)
246         if err:
247             if re.match("machine '.*' is not on", err):
248                 raise InvalidInput("action", "Shutdown", 
249                                    "Machine is not on.")
250             else:
251                 print >> sys.stderr, 'Error on Shutdown:'
252                 print >> sys.stderr, err
253                 raise CodeError('ERROR on remctl')
254     elif action == 'Delete VM':
255         deleteVM(machine)
256
257     d = dict(user=username,
258              command=action,
259              machine=machine)
260     return d
261
262 def resizeDisk(machine_name, disk_name, new_size):
263     remctl("web", "lvresize", machine_name, disk_name, new_size)
264
265 def renameMachine(machine, old_name, new_name):
266     for disk in machine.disks:
267         remctl("web", "lvrename", old_name, 
268                disk.guest_device_name, new_name)
269