1
# Copyright 2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
from __future__ import (
14
'probe_virsh_and_enlist',
17
from lxml import etree
19
import provisioningserver.custom_hardware.utils as utils
22
XPATH_ARCH = "/domain/os/type/@arch"
24
# Virsh stores the architecture with a different
25
# label then MAAS. This maps virsh architecture to
38
class VirshError(Exception):
39
"""Failure communicating to virsh. """
42
class VirshSSH(pexpect.spawn):
45
PROMPT_SSHKEY = "(?i)are you sure you want to continue connecting"
46
PROMPT_PASSWORD = "(?i)(?:password)|(?:passphrase for key)"
47
PROMPT_DENIED = "(?i)permission denied"
48
PROMPT_CLOSED = "(?i)connection closed by remote host"
59
I_PROMPT = PROMPTS.index(PROMPT)
60
I_PROMPT_SSHKEY = PROMPTS.index(PROMPT_SSHKEY)
61
I_PROMPT_PASSWORD = PROMPTS.index(PROMPT_PASSWORD)
63
def __init__(self, timeout=30, maxread=2000):
64
super(VirshSSH, self).__init__(
65
None, timeout=timeout, maxread=maxread)
66
self.name = '<virssh>'
68
def login(self, poweraddr, password=None):
69
"""Starts connection to virsh."""
70
cmd = 'virsh --connect %s' % poweraddr
71
super(VirshSSH, self)._spawn(cmd)
73
i = self.expect(self.PROMPTS, timeout=10)
74
if i == self.I_PROMPT_SSHKEY:
75
# New certificate, lets always accept but if
76
# it changes it will fail to login.
78
i = self.expect(self.EXPECT_PROMPTS)
79
elif i == self.I_PROMPT_PASSWORD:
80
# Requesting password, give it if available.
84
self.sendline(password)
85
i = self.expect(self.EXPECT_PROMPTS)
87
if i != self.I_PROMPT:
88
# Something bad happened, either disconnect,
89
# timeout, wrong password.
95
"""Quits the virsh session."""
99
def prompt(self, timeout=None):
100
"""Waits for virsh prompt."""
102
timeout = self.timeout
103
i = self.expect([self.VIRSH_PROMPT, pexpect.TIMEOUT], timeout=timeout)
112
result = self.before.splitlines()
113
return '\n'.join(result[1:])
116
"""Lists all virtual machines by name."""
117
machines = self.run(['list', '--all', '--name'])
118
return machines.strip().splitlines()
120
def get_state(self, machine):
121
"""Gets the virtual machine state."""
122
state = self.run(['domstate', machine])
123
state = state.strip()
128
def get_mac_addresses(self, machine):
129
"""Gets list of mac addressess assigned to the virtual machine."""
130
output = self.run(['domiflist', machine]).strip()
131
if 'error' in output:
133
output = output.splitlines()[2:]
134
return [line.split()[4] for line in output]
136
def get_arch(self, machine):
137
"""Gets the virtual machine architecture."""
138
output = self.run(['dumpxml', machine]).strip()
139
if 'error' in output:
142
doc = etree.XML(output)
143
evaluator = etree.XPathEvaluator(doc)
144
arch = evaluator(XPATH_ARCH)[0]
146
# Fix architectures that need to be referenced by a different
147
# name, that MAAS understands.
148
return ARCH_FIX.get(arch, arch)
150
def poweron(self, machine):
151
"""Poweron a virtual machine."""
152
output = self.run(['start', machine]).strip()
153
if 'error' in output:
157
def poweroff(self, machine):
158
"""Poweroff a virtual machine."""
159
output = self.run(['destroy', machine]).strip()
160
if 'error' in output:
165
def probe_virsh_and_enlist(poweraddr, password=None):
166
"""Extracts all of the virtual machines from virsh and enlists them
169
:param poweraddr: virsh connection string
172
if not conn.login(poweraddr, password):
173
raise VirshError('Failed to login to virsh console.')
175
for machine in conn.list():
176
arch = conn.get_arch(machine)
177
state = conn.get_state(machine)
178
macs = conn.get_mac_addresses(machine)
180
# Force the machine off, as MAAS will control the machine
181
# and it needs to be in a known state of off.
182
if state == VirshVMState.ON:
183
conn.poweroff(machine)
186
'power_address': poweraddr,
189
if password is not None:
190
params['power_pass'] = password
191
utils.create_node(macs, arch, 'virsh', params)
196
def power_control_virsh(poweraddr, machine, power_change, password=None):
197
"""Powers controls a virtual machine using virsh."""
199
# Force password to None if blank, as the power control
200
# script will send a blank password if one is not set.
205
if not conn.login(poweraddr, password):
206
raise VirshError('Failed to login to virsh console.')
208
state = conn.get_state(machine)
210
raise VirshError('Failed to get domain: %s' % machine)
212
if state == VirshVMState.OFF:
213
if power_change == 'on':
214
if conn.poweron(machine) is False:
215
raise VirshError('Failed to power on domain: %s' % machine)
216
elif state == VirshVMState.ON:
217
if power_change == 'off':
218
if conn.poweroff(machine) is False:
219
raise VirshError('Failed to power off domain: %s' % machine)