This file is indexed.

/usr/lib/python3/dist-packages/provisioningserver/drivers/power/recs.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
# Copyright 2017 christmann informationstechnik + medien GmbH & Co. KG. This
# software is licensed under the GNU Affero General Public License version 3
# (see the file LICENSE).

"""Christmann RECS|Box Power Driver."""

__all__ = []

from typing import Optional
import urllib.error
import urllib.parse
import urllib.request

from lxml.etree import fromstring
from provisioningserver.drivers import (
    make_ip_extractor,
    make_setting_field,
    SETTING_SCOPE,
)
from provisioningserver.drivers.power import (
    PowerConnError,
    PowerDriver,
)
from provisioningserver.logger import get_maas_logger
from provisioningserver.rpc.utils import (
    commission_node,
    create_node,
)
from provisioningserver.utils import typed
from provisioningserver.utils.twisted import synchronous


maaslog = get_maas_logger("drivers.power.recs")


def extract_recs_parameters(context):
    ip = context.get('power_address')
    port = context.get('power_port')
    username = context.get('power_user')
    password = context.get('power_pass')
    node_id = context.get('node_id')
    return ip, port, username, password, node_id


class RECSError(Exception):
    """Failure talking to a RECS_Master."""


class RECSAPI:
    """API to communicate with a RECS_Master"""

    def __init__(self, ip, port, username, password):
        """
        :param ip: The IP address of the RECS_Master
          e.g.: "192.168.0.1"
        :type ip: string
        :param port: The http port to connect to the RECS_Master,
          e.g.: "80"
        :type port: string
        :param username: The username for authentication to RECS_Master,
          e.g.: "admin"
        :type username: string
        :param password: The password for authentication to the RECS_Master,
          e.g.: "admin"
        :type password: string
        """
        self.ip = ip
        self.port = port
        self.username = username
        self.password = password

    def build_url(self, command, params=[]):
        url = 'http://%s:%s/REST/' % (self.ip, self.port)
        params = filter(None, params)
        return urllib.parse.urljoin(url, command) + '?' + '&'.join(params)

    def extract_from_response(self, response, attribute):
        """Extract attribute from first element in response."""
        root = fromstring(response)
        return root.attrib.get(attribute)

    def get(self, command, params=[]):
        """Dispatch a GET request to a RECS_Master."""
        url = self.build_url(command, params)
        authinfo = urllib.request.HTTPPasswordMgrWithDefaultRealm()
        authinfo.add_password(None, url, self.username, self.password)
        proxy_handler = urllib.request.ProxyHandler({})
        auth_handler = urllib.request.HTTPBasicAuthHandler(authinfo)
        opener = urllib.request.build_opener(proxy_handler, auth_handler)
        urllib.request.install_opener(opener)
        try:
            response = urllib.request.urlopen(url)
        except urllib.error.HTTPError as e:
            raise PowerConnError(
                "Could not make proper connection to RECS|Box."
                " HTTP error code: %s" % e.code)
        except urllib.error.URLError as e:
            raise PowerConnError(
                "Could not make proper connection to RECS|Box."
                " Server could not be reached: %s" % e.reason)
        else:
            return response.read()

    def post(self, command, urlparams=[], params={}):
        """Dispatch a POST request to a RECS_Master."""
        url = self.build_url(command, urlparams)
        authinfo = urllib.request.HTTPPasswordMgrWithDefaultRealm()
        authinfo.add_password(None, url, self.username, self.password)
        proxy_handler = urllib.request.ProxyHandler({})
        auth_handler = urllib.request.HTTPBasicAuthHandler(authinfo)
        opener = urllib.request.build_opener(proxy_handler, auth_handler)
        urllib.request.install_opener(opener)
        data = urllib.parse.urlencode(params).encode()
        req = urllib.request.Request(url, data, method='POST')
        try:
            response = urllib.request.urlopen(req)
        except urllib.error.HTTPError as e:
            raise PowerConnError(
                "Could not make proper connection to RECS|Box."
                " HTTP error code: %s" % e.code)
        except urllib.error.URLError as e:
            raise PowerConnError(
                "Could not make proper connection to RECS|Box."
                " Server could not be reached: %s" % e.reason)
        else:
            return response.read()

    def put(self, command, urlparams=[], params={}):
        """Dispatch a PUT request to a RECS_Master."""
        url = self.build_url(command, urlparams)
        authinfo = urllib.request.HTTPPasswordMgrWithDefaultRealm()
        authinfo.add_password(None, url, self.username, self.password)
        proxy_handler = urllib.request.ProxyHandler({})
        auth_handler = urllib.request.HTTPBasicAuthHandler(authinfo)
        opener = urllib.request.build_opener(proxy_handler, auth_handler)
        urllib.request.install_opener(opener)
        data = urllib.parse.urlencode(params).encode()
        req = urllib.request.Request(url, data, method='PUT')
        try:
            response = urllib.request.urlopen(req)
        except urllib.error.HTTPError as e:
            raise PowerConnError(
                "Could not make proper connection to RECS|Box."
                " HTTP error code: %s" % e.code)
        except urllib.error.URLError as e:
            raise PowerConnError(
                "Could not make proper connection to RECS|Box."
                " Server could not be reached: %s" % e.reason)
        else:
            return response.read()

    def get_node_power_state(self, nodeid):
        """Gets the power state of the node."""
        return self.extract_from_response(
            self.get('node/%s' % nodeid), 'state')

    def _set_power(self, nodeid, action):
        """Set power for node."""
        self.post('node/%s/manage/%s' % (nodeid, action))

    def set_power_off_node(self, nodeid):
        """Turns power to node off."""
        return self._set_power(nodeid, 'power_off')

    def set_power_on_node(self, nodeid):
        """Turns power to node on."""
        return self._set_power(nodeid, 'power_on')

    def set_boot_source(self, nodeid, source, persistent):
        """Set boot source of node."""
        self.put('node/%s/manage/set_bootsource' % nodeid,
                 params={'source': source, 'persistent': persistent})

    def get_nodes(self):
        """Gets available nodes.

        Returns dictionary of node IDs, their corresponding
        MAC Addresses and architecture.
        """
        nodes = {}
        xmldata = self.get('node')
        root = fromstring(xmldata)

        # Iterate over all node Elements
        for node_info in root:
            macs = []
            # Add both MACs if available
            macs.append(node_info.attrib.get('macAddressMgmt'))
            macs.append(node_info.attrib.get('macAddressCompute'))
            macs = list(filter(None, macs))
            if macs:
                # Retrive node id
                nodeid = node_info.attrib.get('id')
                # Retrive architecture
                arch = node_info.attrib.get('architecture')
                # Add data for node
                nodes[nodeid] = {'macs': macs, 'arch': arch}

        return nodes


class RECSPowerDriver(PowerDriver):

    name = 'recs_box'
    description = "Christmann RECS|Box Power Driver"
    settings = [
        make_setting_field(
            'node_id', "Node ID", scope=SETTING_SCOPE.NODE,
            required=True),
        make_setting_field('power_address', "Power address", required=True),
        make_setting_field('power_port', "Power port"),
        make_setting_field('power_user', "Power user"),
        make_setting_field(
            'power_pass', "Power password", field_type='password'),
    ]
    ip_extractor = make_ip_extractor('power_address')

    def power_control_recs(
            self, ip, port, username, password, node_id, power_change):
        """Control the power state for the given node."""

        port = 8000 if port is None or port == 0 else port
        api = RECSAPI(ip, port, username, password)

        if power_change == 'on':
            api.set_power_on_node(node_id)
        elif power_change == 'off':
            api.set_power_off_node(node_id)
        else:
            raise RECSError(
                "Unexpected MAAS power mode: %s" % power_change)

    def power_state_recs(self, ip, port, username, password, node_id):
        """Return the power state for the given node."""

        port = 8000 if port is None or port == 0 else port
        api = RECSAPI(ip, port, username, password)

        try:
            power_state = api.get_node_power_state(node_id)
        except urllib.error.HTTPError as e:
            raise RECSError(
                "Failed to retrieve power state. HTTP error code: %s" % e.code)
        except urllib.error.URLError as e:
            raise RECSError(
                "Failed to retrieve power state. Server not reachable: %s"
                % e.reason)

        if power_state == '1':
            return 'on'
        return 'off'

    def set_boot_source_recs(
            self, ip, port, username, password, node_id, source, persistent):
        """Control the boot source for the given node."""

        port = 8000 if port is None or port == 0 else port
        api = RECSAPI(ip, port, username, password)

        api.set_boot_source(node_id, source, persistent)

    def detect_missing_packages(self):
        # uses urllib http client - nothing to look for!
        return []

    def power_on(self, system_id, context):
        """Power on RECS node."""
        power_change = 'on'
        ip, port, username, password, node_id = (
            extract_recs_parameters(context))

        # Set default (persistent) boot to HDD
        self.set_boot_source_recs(
            ip, port, username, password, node_id, "HDD", True)
        # Set next boot to PXE
        self.set_boot_source_recs(
            ip, port, username, password, node_id, "PXE", False)
        self.power_control_recs(
            ip, port, username, password, node_id, power_change)

    def power_off(self, system_id, context):
        """Power off RECS node."""
        power_change = 'off'
        ip, port, username, password, node_id = (
            extract_recs_parameters(context))
        self.power_control_recs(
            ip, port, username, password, node_id, power_change)

    def power_query(self, system_id, context):
        """Power query RECS node."""
        ip, port, username, password, node_id = (
            extract_recs_parameters(context))
        return self.power_state_recs(ip, port, username, password, node_id)


@synchronous
@typed
def probe_and_enlist_recs(
        user: str, ip: str, port: Optional[int], username: Optional[str],
        password: Optional[str], accept_all: bool=False, domain: str=None):
    maaslog.info("Probing for RECS servers as %s@%s", username, ip)

    port = 80 if port is None or port == 0 else port
    api = RECSAPI(ip, port, username, password)

    try:
        # if get_nodes works, we have access to the system
        nodes = api.get_nodes()
    except urllib.error.HTTPError as e:
        raise RECSError(
            "Failed to probe nodes for RECS_Master with ip=%s "
            "port=%s, username=%s, password=%s. HTTP error code: %s"
            % (ip, port, username, password, e.code))
    except urllib.error.URLError as e:
        raise RECSError(
            "Failed to probe nodes for RECS_Master with ip=%s "
            "port=%s, username=%s, password=%s. "
            "Server could not be reached: %s"
            % (ip, port, username, password, e.reason))

    for node_id, data in nodes.items():
        params = {
            'power_address': ip,
            'power_port': port,
            'power_user': username,
            'power_pass': password,
            'node_id': node_id
        }
        arch = 'amd64'
        if data['arch'] == 'arm':
            arch = 'armhf'

        maaslog.info(
            "Creating RECS node %s with MACs: %s", node_id, data['macs'])

        # Set default (persistent) boot to HDD
        api.set_boot_source(node_id, "HDD", True)
        # Set next boot to PXE
        api.set_boot_source(node_id, "PXE", False)

        system_id = create_node(
            data['macs'], arch, 'recs_box', params, domain).wait(30)

        if accept_all:
            commission_node(system_id, user).wait(30)