summaryrefslogtreecommitdiff
path: root/ironic/drivers/modules/amt/common.py
blob: 003453d39301c13488158083fa9bdc68fd3150de (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Common functionalities for AMT Driver
"""
import time
from xml.etree import ElementTree

from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils
import six

from ironic.common import boot_devices
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common import utils


pywsman = importutils.try_import('pywsman')

_SOAP_ENVELOPE = 'http://www.w3.org/2003/05/soap-envelope'

LOG = logging.getLogger(__name__)

REQUIRED_PROPERTIES = {
    'amt_address': _('IP address or host name of the node. Required.'),
    'amt_password': _('Password. Required.'),
    'amt_username': _('Username to log into AMT system. Required.'),
}
OPTIONAL_PROPERTIES = {
    'amt_protocol': _('Protocol used for AMT endpoint. one of http, https; '
                      'default is "http". Optional.'),
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)

opts = [
    cfg.StrOpt('protocol',
               default='http',
               choices=['http', 'https'],
               help=_('Protocol used for AMT endpoint, '
                      'support http/https')),
    cfg.IntOpt('awake_interval',
               default=60,
               min=0,
               help=_('Time interval (in seconds) for successive awake call '
                      'to AMT interface, this depends on the IdleTimeout '
                      'setting on AMT interface. AMT Interface will go to '
                      'sleep after 60 seconds of inactivity by default. '
                      'IdleTimeout=0 means AMT will not go to sleep at all. '
                      'Setting awake_interval=0 will disable awake call.')),
]

CONF = cfg.CONF
opt_group = cfg.OptGroup(name='amt',
                         title='Options for the AMT power driver')
CONF.register_group(opt_group)
CONF.register_opts(opts, opt_group)

# TODO(lintan): More boot devices are supported by AMT, but not useful
# currently. Add them in the future.
BOOT_DEVICES_MAPPING = {
    boot_devices.PXE: 'Intel(r) AMT: Force PXE Boot',
    boot_devices.DISK: 'Intel(r) AMT: Force Hard-drive Boot',
    boot_devices.CDROM: 'Intel(r) AMT: Force CD/DVD Boot',
}
DEFAULT_BOOT_DEVICE = boot_devices.DISK

AMT_PROTOCOL_PORT_MAP = {
    'http': 16992,
    'https': 16993,
}

# ReturnValue constants
RET_SUCCESS = '0'

# A dict cache last awake call to AMT Interface
AMT_AWAKE_CACHE = {}


class Client(object):
    """AMT client.

    Create a pywsman client to connect to the target server
    """
    def __init__(self, address, protocol, username, password):
        port = AMT_PROTOCOL_PORT_MAP[protocol]
        path = '/wsman'
        self.client = pywsman.Client(address, port, path, protocol,
                                     username, password)

    def wsman_get(self, resource_uri, options=None):
        """Get target server info

        :param options: client options
        :param resource_uri: a URI to an XML schema
        :returns: XmlDoc object
        :raises: AMTFailure if get unexpected response.
        :raises: AMTConnectFailure if unable to connect to the server.
        """
        if options is None:
            options = pywsman.ClientOptions()
        doc = self.client.get(options, resource_uri)
        item = 'Fault'
        fault = xml_find(doc, _SOAP_ENVELOPE, item)
        if fault is not None:
            LOG.exception(_LE('Call to AMT with URI %(uri)s failed: '
                              'got Fault %(fault)s'),
                          {'uri': resource_uri, 'fault': fault.text})
            raise exception.AMTFailure(cmd='wsman_get')
        return doc

    def wsman_invoke(self, options, resource_uri, method, data=None):
        """Invoke method on target server

        :param options: client options
        :param resource_uri: a URI to an XML schema
        :param method: invoke method
        :param data: a XmlDoc as invoke input
        :returns: XmlDoc object
        :raises: AMTFailure if get unexpected response.
        :raises: AMTConnectFailure if unable to connect to the server.
        """
        if data is None:
            doc = self.client.invoke(options, resource_uri, method)
        else:
            doc = self.client.invoke(options, resource_uri, method, data)
        item = "ReturnValue"
        return_value = xml_find(doc, resource_uri, item).text
        if return_value != RET_SUCCESS:
            LOG.exception(_LE("Call to AMT with URI %(uri)s and "
                              "method %(method)s failed: return value "
                              "was %(value)s"),
                          {'uri': resource_uri, 'method': method,
                           'value': return_value})
            raise exception.AMTFailure(cmd='wsman_invoke')
        return doc


def parse_driver_info(node):
    """Parses and creates AMT driver info

    :param node: an Ironic node object.
    :returns: AMT driver info.
    :raises: MissingParameterValue if any required parameters are missing.
    :raises: InvalidParameterValue if any parameters have invalid values.
    """

    info = node.driver_info or {}
    d_info = {}
    missing_info = []

    for param in REQUIRED_PROPERTIES:
        value = info.get(param)
        if value:
            if not isinstance(value, six.binary_type):
                value = value.encode()
            d_info[param[4:]] = value
        else:
            missing_info.append(param)

    if missing_info:
        raise exception.MissingParameterValue(_(
            "AMT driver requires the following to be set in "
            "node's driver_info: %s.") % missing_info)

    d_info['uuid'] = node.uuid
    param = 'amt_protocol'
    protocol = info.get(param, CONF.amt.get(param[4:]))
    if protocol not in AMT_PROTOCOL_PORT_MAP:
        raise exception.InvalidParameterValue(
            _("Invalid protocol %s.") % protocol)
    if not isinstance(value, six.binary_type):
        protocol = protocol.encode()
    d_info[param[4:]] = protocol

    return d_info


def get_wsman_client(node):
    """Return a AMT Client object

    :param node: an Ironic node object.
    :returns: a Client object
    :raises: MissingParameterValue if any required parameters are missing.
    :raises: InvalidParameterValue if any parameters have invalid values.
    """
    driver_info = parse_driver_info(node)
    client = Client(address=driver_info['address'],
                    protocol=driver_info['protocol'],
                    username=driver_info['username'],
                    password=driver_info['password'])
    return client


def xml_find(doc, namespace, item):
    """Find the first element with namespace and item, in the XML doc

    :param doc: a doc object.
    :param namespace: the namespace of the element.
    :param item: the element name.
    :returns: the element object or None
    :raises: AMTConnectFailure if unable to connect to the server.
    """
    if doc is None:
        raise exception.AMTConnectFailure()
    tree = ElementTree.fromstring(doc.root().string())
    query = ('.//{%(namespace)s}%(item)s' % {'namespace': namespace,
                                             'item': item})
    return tree.find(query)


def awake_amt_interface(node):
    """Wake up AMT interface.

    AMT interface goes to sleep after a period of time if the host is off.
    This method will ping AMT interface to wake it up. Because there is
    no guarantee that the AMT address in driver_info is correct, only
    ping the IP five times which is enough to wake it up.

    :param node: an Ironic node object.
    :raises: AMTConnectFailure if unable to connect to the server.
    """
    awake_interval = CONF.amt.awake_interval
    if awake_interval == 0:
        return

    now = time.time()
    last_awake = AMT_AWAKE_CACHE.get(node.uuid, 0)
    if now - last_awake > awake_interval:
        cmd_args = ['ping', '-i', 0.2, '-c', 5,
                    node.driver_info['amt_address']]
        try:
            utils.execute(*cmd_args)
        except processutils.ProcessExecutionError as err:
            LOG.error(_LE('Unable to awake AMT interface on node '
                          '%(node_id)s. Error: %(error)s'),
                      {'node_id': node.uuid, 'error': err})
            raise exception.AMTConnectFailure()
        else:
            LOG.debug(('Successfully awakened AMT interface on node '
                       '%(node_id)s.'), {'node_id': node.uuid})
            AMT_AWAKE_CACHE[node.uuid] = now