Actually generalize the invirt.remctl module
[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, *args,
28                       principal='daemon/'+config.web.hostname,
29                       **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 deleteVM(machine):
175     """Delete a VM."""
176     remctl('control', machine.name, 'destroy', err=True)
177     session.begin()
178     delete_disk_pairs = [(machine.name, d.guest_device_name) 
179                          for d in machine.disks]
180     try:
181         for mname, dname in delete_disk_pairs:
182             remctl('web', 'lvremove', mname, dname)
183         for nic in machine.nics:
184             nic.machine_id = None
185             nic.hostname = None
186             session.save_or_update(nic)
187         for disk in machine.disks:
188             session.delete(disk)
189         session.delete(machine)
190         session.commit()
191     except:
192         session.rollback()
193         raise
194
195 def commandResult(username, state, fields):
196     start_time = 0
197     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
198     action = fields.getfirst('action')
199     cdrom = fields.getfirst('cdrom')
200     if cdrom is not None and not CDROM.query().filter_by(cdrom_id=cdrom).one():
201         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
202     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
203                       'Delete VM'):
204         raise CodeError("Invalid action '%s'" % action)
205     if action == 'Reboot':
206         if cdrom is not None:
207             out, err = remctl('control', machine.name, 'reboot', cdrom,
208                               err=True)
209         else:
210             out, err = remctl('control', machine.name, 'reboot',
211                               err=True)
212         if err:
213             if re.match("machine '.*' is not on", err):
214                 raise InvalidInput("action", "reboot", 
215                                    "Machine is not on")
216             else:
217                 print >> sys.stderr, 'Error on reboot:'
218                 print >> sys.stderr, err
219                 raise CodeError('ERROR on remctl')
220                 
221     elif action == 'Power on':
222         if validation.maxMemory(username, state, machine) < machine.memory:
223             raise InvalidInput('action', 'Power on',
224                                "You don't have enough free RAM quota "
225                                "to turn on this machine.")
226         bootMachine(machine, cdrom)
227     elif action == 'Power off':
228         out, err = remctl('control', machine.name, 'destroy', err=True)
229         if err:
230             if re.match("machine '.*' is not on", err):
231                 raise InvalidInput("action", "Power off", 
232                                    "Machine is not on.")
233             else:
234                 print >> sys.stderr, 'Error on power off:'
235                 print >> sys.stderr, err
236                 raise CodeError('ERROR on remctl')
237     elif action == 'Shutdown':
238         out, err = remctl('control', machine.name, 'shutdown', err=True)
239         if err:
240             if re.match("machine '.*' is not on", err):
241                 raise InvalidInput("action", "Shutdown", 
242                                    "Machine is not on.")
243             else:
244                 print >> sys.stderr, 'Error on Shutdown:'
245                 print >> sys.stderr, err
246                 raise CodeError('ERROR on remctl')
247     elif action == 'Delete VM':
248         deleteVM(machine)
249
250     d = dict(user=username,
251              command=action,
252              machine=machine)
253     return d
254
255 def resizeDisk(machine_name, disk_name, new_size):
256     remctl("web", "lvresize", machine_name, disk_name, new_size)
257
258 def renameMachine(machine, old_name, new_name):
259     for disk in machine.disks:
260         remctl("web", "lvrename", old_name, 
261                disk.guest_device_name, new_name)
262