~blake-rouse/maas/osystem-preseed-cleanup

« back to all changes in this revision

Viewing changes to src/provisioningserver/custom_hardware/virsh.py

Merge trunk, fix doc string for compose_preseed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
from __future__ import (
 
5
    absolute_import,
 
6
    print_function,
 
7
    unicode_literals,
 
8
    )
 
9
 
 
10
str = None
 
11
 
 
12
__metaclass__ = type
 
13
__all__ = [
 
14
    'probe_virsh_and_enlist',
 
15
    ]
 
16
 
 
17
from lxml import etree
 
18
import pexpect
 
19
import provisioningserver.custom_hardware.utils as utils
 
20
 
 
21
 
 
22
XPATH_ARCH = "/domain/os/type/@arch"
 
23
 
 
24
# Virsh stores the architecture with a different
 
25
# label then MAAS. This maps virsh architecture to
 
26
# MAAS architecture.
 
27
ARCH_FIX = {
 
28
    'x86_64': 'amd64',
 
29
    'ppc64': 'ppc64el',
 
30
    }
 
31
 
 
32
 
 
33
class VirshVMState:
 
34
    OFF = "shut off"
 
35
    ON = "running"
 
36
 
 
37
 
 
38
class VirshError(Exception):
 
39
    """Failure communicating to virsh. """
 
40
 
 
41
 
 
42
class VirshSSH(pexpect.spawn):
 
43
 
 
44
    PROMPT = r"virsh \#"
 
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"
 
49
 
 
50
    PROMPTS = [
 
51
        PROMPT_SSHKEY,
 
52
        PROMPT_PASSWORD,
 
53
        PROMPT,
 
54
        PROMPT_DENIED,
 
55
        pexpect.TIMEOUT,
 
56
        PROMPT_CLOSED
 
57
    ]
 
58
 
 
59
    I_PROMPT = PROMPTS.index(PROMPT)
 
60
    I_PROMPT_SSHKEY = PROMPTS.index(PROMPT_SSHKEY)
 
61
    I_PROMPT_PASSWORD = PROMPTS.index(PROMPT_PASSWORD)
 
62
 
 
63
    def __init__(self, timeout=30, maxread=2000):
 
64
        super(VirshSSH, self).__init__(
 
65
            None, timeout=timeout, maxread=maxread)
 
66
        self.name = '<virssh>'
 
67
 
 
68
    def login(self, poweraddr, password=None):
 
69
        """Starts connection to virsh."""
 
70
        cmd = 'virsh --connect %s' % poweraddr
 
71
        super(VirshSSH, self)._spawn(cmd)
 
72
 
 
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.
 
77
            self.sendline("yes")
 
78
            i = self.expect(self.EXPECT_PROMPTS)
 
79
        elif i == self.I_PROMPT_PASSWORD:
 
80
            # Requesting password, give it if available.
 
81
            if password is None:
 
82
                self.close()
 
83
                return False
 
84
            self.sendline(password)
 
85
            i = self.expect(self.EXPECT_PROMPTS)
 
86
 
 
87
        if i != self.I_PROMPT:
 
88
            # Something bad happened, either disconnect,
 
89
            # timeout, wrong password.
 
90
            self.close()
 
91
            return False
 
92
        return True
 
93
 
 
94
    def logout(self):
 
95
        """Quits the virsh session."""
 
96
        self.sendline("quit")
 
97
        self.close()
 
98
 
 
99
    def prompt(self, timeout=None):
 
100
        """Waits for virsh prompt."""
 
101
        if timeout is None:
 
102
            timeout = self.timeout
 
103
        i = self.expect([self.VIRSH_PROMPT, pexpect.TIMEOUT], timeout=timeout)
 
104
        if i == 1:
 
105
            return False
 
106
        return True
 
107
 
 
108
    def run(self, args):
 
109
        cmd = ' '.join(args)
 
110
        self.sendline(cmd)
 
111
        self.prompt()
 
112
        result = self.before.splitlines()
 
113
        return '\n'.join(result[1:])
 
114
 
 
115
    def list(self):
 
116
        """Lists all virtual machines by name."""
 
117
        machines = self.run(['list', '--all', '--name'])
 
118
        return machines.strip().splitlines()
 
119
 
 
120
    def get_state(self, machine):
 
121
        """Gets the virtual machine state."""
 
122
        state = self.run(['domstate', machine])
 
123
        state = state.strip()
 
124
        if 'error' in state:
 
125
            return None
 
126
        return state
 
127
 
 
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:
 
132
            return None
 
133
        output = output.splitlines()[2:]
 
134
        return [line.split()[4] for line in output]
 
135
 
 
136
    def get_arch(self, machine):
 
137
        """Gets the virtual machine architecture."""
 
138
        output = self.run(['dumpxml', machine]).strip()
 
139
        if 'error' in output:
 
140
            return None
 
141
 
 
142
        doc = etree.XML(output)
 
143
        evaluator = etree.XPathEvaluator(doc)
 
144
        arch = evaluator(XPATH_ARCH)[0]
 
145
 
 
146
        # Fix architectures that need to be referenced by a different
 
147
        # name, that MAAS understands.
 
148
        return ARCH_FIX.get(arch, arch)
 
149
 
 
150
    def poweron(self, machine):
 
151
        """Poweron a virtual machine."""
 
152
        output = self.run(['start', machine]).strip()
 
153
        if 'error' in output:
 
154
            return False
 
155
        return True
 
156
 
 
157
    def poweroff(self, machine):
 
158
        """Poweroff a virtual machine."""
 
159
        output = self.run(['destroy', machine]).strip()
 
160
        if 'error' in output:
 
161
            return False
 
162
        return True
 
163
 
 
164
 
 
165
def probe_virsh_and_enlist(poweraddr, password=None):
 
166
    """Extracts all of the virtual machines from virsh and enlists them
 
167
    into MAAS.
 
168
 
 
169
    :param poweraddr: virsh connection string
 
170
    """
 
171
    conn = VirshSSH()
 
172
    if not conn.login(poweraddr, password):
 
173
        raise VirshError('Failed to login to virsh console.')
 
174
 
 
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)
 
179
 
 
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)
 
184
 
 
185
        params = {
 
186
            'power_address': poweraddr,
 
187
            'power_id': machine,
 
188
        }
 
189
        if password is not None:
 
190
            params['power_pass'] = password
 
191
        utils.create_node(macs, arch, 'virsh', params)
 
192
 
 
193
    conn.logout()
 
194
 
 
195
 
 
196
def power_control_virsh(poweraddr, machine, power_change, password=None):
 
197
    """Powers controls a virtual machine using virsh."""
 
198
 
 
199
    # Force password to None if blank, as the power control
 
200
    # script will send a blank password if one is not set.
 
201
    if password == '':
 
202
        password = None
 
203
 
 
204
    conn = VirshSSH()
 
205
    if not conn.login(poweraddr, password):
 
206
        raise VirshError('Failed to login to virsh console.')
 
207
 
 
208
    state = conn.get_state(machine)
 
209
    if state is None:
 
210
        raise VirshError('Failed to get domain: %s' % machine)
 
211
 
 
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)