This file is indexed.

/usr/lib/python3/dist-packages/provisioningserver/drivers/power/__init__.py is in python3-maas-provisioningserver 2.4.0~beta2-6865-gec43e47e6-0ubuntu1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# Copyright 2014-2016 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Base power driver."""

__all__ = [
    "is_power_parameter_set",
    "POWER_QUERY_TIMEOUT",
    "PowerActionError",
    "PowerAuthError",
    "PowerConnError",
    "PowerDriver",
    "PowerDriverBase",
    "PowerError",
    "PowerFatalError",
    "PowerSettingError",
    "PowerToolError",
    ]

from abc import (
    ABCMeta,
    abstractmethod,
    abstractproperty,
)
from datetime import timedelta
import sys

from jsonschema import validate
from provisioningserver.drivers import (
    IP_EXTRACTOR_SCHEMA,
    SETTING_PARAMETER_FIELD_SCHEMA,
)
from provisioningserver.utils.twisted import (
    IAsynchronous,
    pause,
)
from twisted.internet import reactor
from twisted.internet.defer import (
    inlineCallbacks,
    returnValue,
)
from twisted.internet.threads import deferToThread

# We specifically declare this here so that a node not knowing its own
# powertype won't fail to enlist. However, we don't want it in the list
# of power types since setting a node's power type to "I don't know"
# from another type doens't make any sense.
UNKNOWN_POWER_TYPE = ''

# A policy used when waiting between retries of power changes.
DEFAULT_WAITING_POLICY = (1, 2, 2, 4, 6, 8, 12)

# JSON schema for what a power driver definition should look like
JSON_POWER_DRIVER_SCHEMA = {
    'title': "Power driver setting set",
    'type': 'object',
    'properties': {
        'driver_type': {
            'type': 'string',
        },
        'name': {
            'type': 'string',
        },
        'description': {
            'type': 'string',
        },
        'fields': {
            'type': 'array',
            'items': SETTING_PARAMETER_FIELD_SCHEMA,
        },
        'ip_extractor': IP_EXTRACTOR_SCHEMA,
        'queryable': {
            'type': 'boolean',
        },
        'missing_packages': {
            'type': 'array',
            'items': {
                'type': 'string',
            },
        },
    },
    'required': ['driver_type', 'name', 'description', 'fields'],
}

# JSON schema for multiple power drivers.
JSON_POWER_DRIVERS_SCHEMA = {
    'title': "Power drivers parameters set",
    'type': 'array',
    'items': JSON_POWER_DRIVER_SCHEMA,
}


# Timeout for the power query action. We might be holding up a thread for that
# long but some BMCs (notably seamicro) can take a long time to respond to
# a power query request.
# This should be configurable per-BMC.
POWER_QUERY_TIMEOUT = timedelta(seconds=45).total_seconds()


def is_power_parameter_set(param):
    return not (param is None or param == "" or param.isspace())


class PowerError(Exception):
    """Base error for all power driver failure commands."""


class PowerFatalError(PowerError):
    """Error that is raised when the power action should not continue to
    retry at all.

    This exception will cause the power action to fail instantly,
    without retrying.
    """


class PowerSettingError(PowerFatalError):
    """Error that is raised when the power type is missing argument
    that is required to control the BMC.

    This exception will cause the power action to fail instantly,
    without retrying.
    """


class PowerToolError(PowerFatalError):
    """Error that is raised when the power tool is missing completely
    for use.

    This exception will cause the power action to fail instantly,
    without retrying.
    """


class PowerAuthError(PowerFatalError):
    """Error raised when power driver fails to authenticate to BMC.

    This exception will cause the power action to fail instantly,
    without retrying.
    """


class PowerConnError(PowerError):
    """Error raised when power driver fails to communicate to BMC."""


class PowerActionError(PowerError):
    """Error when actually performing an action on the BMC, like `on`
    or `off`."""


class PowerDriverBase(metaclass=ABCMeta):
    """Base driver for a power driver."""

    def __init__(self):
        super(PowerDriverBase, self).__init__()
        validate(
            self.get_schema(detect_missing_packages=False),
            JSON_POWER_DRIVER_SCHEMA)

    @abstractproperty
    def name(self):
        """Name of the power driver."""

    @abstractproperty
    def description(self):
        """Description of the power driver."""

    @abstractproperty
    def settings(self):
        """List of settings for the driver.

        Each setting in this list will be different per user. They are passed
        to the `on`, `off`, and `query` using the context. It is up
        to the driver to read these options before performing the operation.
        """

    @abstractproperty
    def ip_extractor(self):
        """IP extractor.

        Name of the settings field and python REGEX pattern for extracting IP
        the address from the value.
        """

    @abstractproperty
    def queryable(self):
        """Whether or not the power driver is queryable."""

    @abstractmethod
    def detect_missing_packages(self):
        """Implement this method for the actual implementation
        of the check for the driver's missing support packages.
        """

    @abstractmethod
    def on(self, system_id, context):
        """Perform the power on action for `system_id`.

        :param system_id: `Node.system_id`
        :param context: Power settings for the node.
        """

    @abstractmethod
    def off(self, system_id, context):
        """Perform the power off action for `system_id`.

        :param system_id: `Node.system_id`
        :param context: Power settings for the node.
        """

    @abstractmethod
    def cycle(self, system_id, context):
        """Perform the cycle action for `system_id`.

        :param system_id: `Node.system_id`
        :param context: Power settings for the node.
        """

    @abstractmethod
    def query(self, system_id, context):
        """Perform the query action for `system_id`.

        :param system_id: `Node.system_id`
        :param context: Power settings for the node.
        :return: status of power on BMC. `on` or `off`.
        :raises PowerError: states unable to get status from BMC. It is
            up to this method to report the actual issue to the Region. The
            calling function should ignore this error, and continue on.
        """

    def get_schema(self, detect_missing_packages=True):
        """Returns the JSON schema for the driver.

        Calculates the missing packages on each invoke.
        """
        schema = dict(
            driver_type='power', name=self.name, description=self.description,
            fields=self.settings, queryable=self.queryable,
            missing_packages=(
                self.detect_missing_packages()
                if detect_missing_packages else []))
        if self.ip_extractor is not None:
            schema['ip_extractor'] = self.ip_extractor
        return schema

    def get_setting(self, name):
        """Return the setting field by its name."""
        for setting in self.settings:
            if setting['name'] == name:
                return setting
        return None


def get_error_message(err):
    """Returns the proper error message based on error."""
    if isinstance(err, PowerAuthError):
        return "Could not authenticate to node's BMC: %s" % err
    elif isinstance(err, PowerConnError):
        return "Could not contact node's BMC: %s" % err
    elif isinstance(err, PowerSettingError):
        return "Missing or invalid power setting: %s" % err
    elif isinstance(err, PowerToolError):
        return "Missing power tool: %s" % err
    elif isinstance(err, PowerActionError):
        return "Failed to complete power action: %s" % err
    else:
        return "Failed talking to node's BMC: %s" % err


class PowerDriver(PowerDriverBase):
    """Default power driver logic."""

    wait_time = DEFAULT_WAITING_POLICY
    queryable = True

    def __init__(self, clock=reactor):
        self.clock = reactor

    @abstractmethod
    def power_on(self, system_id, context):
        """Implement this method for the actual implementation
        of the power on command.
        """

    @abstractmethod
    def power_off(self, system_id, context):
        """Implement this method for the actual implementation
        of the power off command.
        """

    @abstractmethod
    def power_query(self, system_id, context):
        """Implement this method for the actual implementation
        of the power query command."""

    def on(self, system_id, context):
        """Performs the power on action for `system_id`.

        Do not override `on` method unless you want to provide custom logic on
        how retries and error detection is handled. Override `power_on` for
        just the power on action, and `on` will handle the retrying.
        """
        return self.perform_power(self.power_on, "on", system_id, context)

    def off(self, system_id, context):
        """Performs the power off action for `system_id`.

        Do not override `off` method unless you want to provide custom logic on
        how retries and error detection is handled. Override `power_off` for
        just the power off action, and `off` will handle the retrying and error
        reporting.
        """
        return self.perform_power(self.power_off, "off", system_id, context)

    @inlineCallbacks
    def cycle(self, system_id, context):
        """Performs the power cycle action for `system_id`.

        Do not override `cycle` method unless you want to provide custom logic
        on how retries and error detection is handled.
        """
        state = yield self.query(system_id, context)
        if state == 'on':
            yield self.perform_power(self.power_off, "off", system_id, context)
        yield self.perform_power(self.power_on, "on", system_id, context)

    @inlineCallbacks
    def query(self, system_id, context):
        """Performs the power query action for `system_id`."""
        exc_info = None, None, None
        for waiting_time in self.wait_time:
            try:
                # Power queries are predominantly transactional and thus
                # blocking/synchronous. Genuinely non-blocking/asynchronous
                # methods must out themselves explicitly.
                if IAsynchronous.providedBy(self.power_query):
                    # The @asynchronous decorator will DTRT.
                    state = yield self.power_query(system_id, context)
                else:
                    state = yield deferToThread(
                        self.power_query, system_id, context)
            except PowerFatalError:
                raise  # Don't retry.
            except PowerError:
                exc_info = sys.exc_info()
                # Wait before retrying.
                yield pause(waiting_time, self.clock)
            else:
                returnValue(state)
        else:
            raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])

    @inlineCallbacks
    def perform_power(self, power_func, state_desired, system_id, context):
        """Provides the logic to perform the power actions.

        :param power_func: Function used to change the power state of the
            node. Typically this will be `self.power_on` or `self.power_off`.
        :param state_desired: The desired state for this node to be in,
            typically "on" or "off".
        :param system_id: The node's system ID.
        """

        state = "unknown"
        exc_info = None, None, None

        for waiting_time in self.wait_time:
            # Try to change state.
            try:
                # Power methods are predominantly transactional and thus
                # blocking/synchronous. Genuinely non-blocking/asynchronous
                # methods must out themselves explicitly.
                if IAsynchronous.providedBy(power_func):
                    # The @asynchronous decorator will DTRT.
                    yield power_func(system_id, context)
                else:
                    yield deferToThread(
                        power_func, system_id, context)
            except PowerFatalError:
                raise  # Don't retry.
            except PowerError:
                exc_info = sys.exc_info()
                # Wait before retrying.
                yield pause(waiting_time, self.clock)
            else:
                # Wait before checking state.
                yield pause(waiting_time, self.clock)
                # Try to get power state.
                try:
                    # Power queries are predominantly transactional and thus
                    # blocking/synchronous. Genuinely non-blocking/asynchronous
                    # methods must out themselves explicitly.
                    if IAsynchronous.providedBy(self.power_query):
                        # The @asynchronous decorator will DTRT.
                        state = yield self.power_query(system_id, context)
                    else:
                        state = yield deferToThread(
                            self.power_query, system_id, context)
                except PowerFatalError:
                    raise  # Don't retry.
                except PowerError:
                    exc_info = sys.exc_info()
                else:
                    # If state is now the correct state, done.
                    if state == state_desired:
                        return

        if exc_info == (None, None, None):
            # No error found, so communication to the BMC is good, state must
            # have not changed in the elapsed time. That is the only reason we
            # should make it this far.
            raise PowerError(
                "Failed to power %s. BMC never transitioned from %s to %s."
                % (system_id, state, state_desired))
        else:
            # Report the last error.
            raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])