~allenap/maas/xxx-a-thon

« back to all changes in this revision

Viewing changes to src/provisioningserver/drivers/hardware/msftocs.py

  • Committer: LaMont Jones
  • Date: 2016-03-07 23:20:52 UTC
  • mfrom: (4657.1.84 maas)
  • mto: (4657.1.93 maas)
  • mto: This revision was merged to the branch mainline in revision 4660.
  • Revision ID: lamont@canonical.com-20160307232052-rgfxbq7dujj6s093
MergeĀ fromĀ trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2015 Canonical Ltd.  This software is licensed under the
2
 
# GNU Affero General Public License version 3 (see the file LICENSE).
3
 
 
4
 
__all__ = [
5
 
    'power_control_msftocs',
6
 
    'power_state_msftocs',
7
 
    'probe_and_enlist_msftocs',
8
 
    ]
9
 
 
10
 
import urllib.error
11
 
import urllib.parse
12
 
import urllib.request
13
 
 
14
 
from lxml.etree import fromstring
15
 
from provisioningserver.rpc.utils import (
16
 
    commission_node,
17
 
    create_node,
18
 
)
19
 
from provisioningserver.utils.twisted import synchronous
20
 
 
21
 
 
22
 
class MicrosoftOCSState(object):
23
 
    ON = "ON"
24
 
    OFF = "OFF"
25
 
 
26
 
 
27
 
class MicrosoftOCSError(Exception):
28
 
    """Failure talking to a MicrosoftOCS chassis controller. """
29
 
 
30
 
 
31
 
class MicrosoftOCSAPI(object):
32
 
    """API to communicate with the Microsoft OCS Chassis Manager."""
33
 
 
34
 
    def __init__(self, ip, port, username, password):
35
 
        """
36
 
        :param ip: The IP address of the MicrosoftOCS chassis,
37
 
          e.g.: "192.168.0.1"
38
 
        :type ip: string
39
 
        :param port: The http port to connect to the MicrosoftOCS chassis,
40
 
          e.g.: "8000"
41
 
        :type port: string
42
 
        :param username: The username for authentication to the MicrosoftOCS
43
 
          chassis, e.g.: "admin"
44
 
        :type username: string
45
 
        :param password: The password for authentication to the MicrosoftOCS
46
 
          chassis, e.g.: "password"
47
 
        :type password: string
48
 
        """
49
 
        self.ip = ip
50
 
        self.port = port
51
 
        self.username = username
52
 
        self.password = password
53
 
 
54
 
    def build_url(self, command, params=[]):
55
 
        url = 'http://%s:%d/' % (self.ip, self.port)
56
 
        params = [param for param in params if bool(param)]
57
 
        return urllib.parse.urljoin(url, command) + '?' + '&'.join(params)
58
 
 
59
 
    def extract_from_response(self, response, element_tag):
60
 
        """Extract text from first element with element_tag in response."""
61
 
        root = fromstring(response)
62
 
        return root.findtext(
63
 
            './/ns:%s' % element_tag,
64
 
            namespaces={'ns': root.nsmap[None]})
65
 
 
66
 
    def get(self, command, params=None):
67
 
        """Dispatch a GET request to a Microsoft OCS chassis."""
68
 
        url = self.build_url(command, params)
69
 
        authinfo = urllib.request.HTTPPasswordMgrWithDefaultRealm()
70
 
        authinfo.add_password(None, url, self.username, self.password)
71
 
        proxy_handler = urllib.request.ProxyHandler({})
72
 
        auth_handler = urllib.request.HTTPBasicAuthHandler(authinfo)
73
 
        opener = urllib.request.build_opener(proxy_handler, auth_handler)
74
 
        urllib.request.install_opener(opener)
75
 
        response = urllib.request.urlopen(url)
76
 
        return response.read()
77
 
 
78
 
    def get_blade_power_state(self, bladeid):
79
 
        """Gets the ON/OFF State of Blade."""
80
 
        params = ["bladeid=%s" % bladeid]
81
 
        return self.extract_from_response(
82
 
            self.get('GetBladeState', params), 'bladeState')
83
 
 
84
 
    def _set_power(self, bladeid, element_tag):
85
 
        """Set AC Outlet Power for Blade."""
86
 
        params = ["bladeid=%s" % bladeid]
87
 
        return self.extract_from_response(
88
 
            self.get(element_tag, params), 'completionCode')
89
 
 
90
 
    def set_power_off_blade(self, bladeid):
91
 
        """Turns AC Outlet Power OFF for Blade."""
92
 
        return self._set_power(bladeid, 'SetBladeOff')
93
 
 
94
 
    def set_power_on_blade(self, bladeid):
95
 
        """Turns AC Outlet Power ON for Blade."""
96
 
        return self._set_power(bladeid, 'SetBladeOn')
97
 
 
98
 
    def set_next_boot_device(self, bladeid, pxe=False,
99
 
                             uefi=False, persistent=False):
100
 
        """Set Next Boot Device."""
101
 
        boot_pxe = '2' if pxe else '3'
102
 
        boot_uefi = 'true' if uefi else 'false'
103
 
        boot_persistent = 'true' if persistent else 'false'
104
 
        params = [
105
 
            "bladeid=%s" % bladeid, "bootType=%s" % boot_pxe,
106
 
            "uefi=%s" % boot_uefi, "persistent=%s" % boot_persistent
107
 
        ]
108
 
        return self.extract_from_response(
109
 
            self.get('SetNextBoot', params), 'nextBoot')
110
 
 
111
 
    def get_blades(self):
112
 
        """Gets available Blades.
113
 
 
114
 
        Returns dictionary of blade numbers and their corresponding
115
 
        MAC Addresses.
116
 
        """
117
 
        blades = {}
118
 
        root = fromstring(self.get('GetChassisInfo'))
119
 
        namespace = {'ns': root.nsmap[None]}
120
 
        blade_collections = root.find(
121
 
            './/ns:bladeCollections', namespaces=namespace)
122
 
        # Iterate over all BladeInfo Elements
123
 
        for blade_info in blade_collections:
124
 
            blade_mac_address = blade_info.find(
125
 
                './/ns:bladeMacAddress', namespaces=namespace)
126
 
            macs = []
127
 
            # Iterate over all NicInfo Elements and add MAC Addresses
128
 
            for nic_info in blade_mac_address:
129
 
                macs.append(
130
 
                    nic_info.findtext(
131
 
                        './/ns:macAddress', namespaces=namespace))
132
 
            macs = [mac for mac in macs if bool(mac)]
133
 
            if macs:
134
 
                # Retrive Blade id number
135
 
                bladeid = blade_info.findtext(
136
 
                    './/ns:bladeNumber', namespaces=namespace)
137
 
                # Add MAC Addresses for Blade
138
 
                blades[bladeid] = macs
139
 
 
140
 
        return blades
141
 
 
142
 
 
143
 
def power_state_msftocs(ip, port, username, password, blade_id):
144
 
    """Return the power state for the given Blade."""
145
 
 
146
 
    port = int(port) or 8000  # Default Port for MicrosoftOCS Chassis is 8000
147
 
    api = MicrosoftOCSAPI(ip, port, username, password)
148
 
 
149
 
    try:
150
 
        power_state = api.get_blade_power_state(blade_id)
151
 
    except urllib.error.HTTPError as e:
152
 
        raise MicrosoftOCSError(
153
 
            "Failed to retrieve power state. HTTP error code: %s" % e.code)
154
 
    except urllib.error.URLError as e:
155
 
        raise MicrosoftOCSError(
156
 
            "Failed to retrieve power state. Server could not be reached: %s"
157
 
            % e.reason)
158
 
 
159
 
    if power_state == MicrosoftOCSState.OFF:
160
 
        return 'off'
161
 
    elif power_state == MicrosoftOCSState.ON:
162
 
        return 'on'
163
 
    raise MicrosoftOCSError('Unknown power state: %s' % power_state)
164
 
 
165
 
 
166
 
def power_control_msftocs(
167
 
        ip, port, username, password, blade_id, power_change):
168
 
    """Control the power state for the given Blade."""
169
 
 
170
 
    port = int(port) or 8000  # Default Port for MicrosoftOCS Chassis is 8000
171
 
    api = MicrosoftOCSAPI(ip, port, username, password)
172
 
 
173
 
    if power_change == 'on':
174
 
        power_state = api.get_blade_power_state(blade_id)
175
 
        if power_state == MicrosoftOCSState.ON:
176
 
            api.set_power_off_blade(blade_id)
177
 
        # Set default (persistent) boot to HDD
178
 
        api.set_next_boot_device(blade_id, persistent=True)
179
 
        # Set next boot to PXE
180
 
        api.set_next_boot_device(blade_id, pxe=True)
181
 
        api.set_power_on_blade(blade_id)
182
 
    elif power_change == 'off':
183
 
        api.set_power_off_blade(blade_id)
184
 
    else:
185
 
        raise MicrosoftOCSError(
186
 
            "Unexpected MAAS power mode: %s" % power_change)
187
 
 
188
 
 
189
 
@synchronous
190
 
def probe_and_enlist_msftocs(
191
 
        user, ip, port, username, password, accept_all=False):
192
 
    """ Extracts all of nodes from msftocs, sets all of them to boot via
193
 
    HDD by, default, sets them to bootonce via PXE, and then enlists them
194
 
    into MAAS.
195
 
    """
196
 
    port = int(port) or 8000  # Default Port for MicrosoftOCS Chassis is 8000
197
 
    api = MicrosoftOCSAPI(ip, port, username, password)
198
 
 
199
 
    try:
200
 
        # if get_blades works, we have access to the system
201
 
        blades = api.get_blades()
202
 
    except urllib.error.HTTPError as e:
203
 
        raise MicrosoftOCSError(
204
 
            "Failed to probe nodes for Microsoft OCS with ip=%s "
205
 
            "port=%d, username=%s, password=%s. HTTP error code: %s"
206
 
            % (ip, port, username, password, e.code))
207
 
    except urllib.error.URLError as e:
208
 
        raise MicrosoftOCSError(
209
 
            "Failed to probe nodes for Microsoft OCS with ip=%s "
210
 
            "port=%d, username=%s, password=%s. "
211
 
            "Server could not be reached: %s"
212
 
            % (ip, port, username, password, e.reason))
213
 
 
214
 
    for blade_id, macs in blades.items():
215
 
        # Set default (persistent) boot to HDD
216
 
        api.set_next_boot_device(blade_id, persistent=True)
217
 
        # Set next boot to PXE
218
 
        api.set_next_boot_device(blade_id, pxe=True)
219
 
        params = {
220
 
            'power_address': ip,
221
 
            'power_port': port,
222
 
            'power_user': username,
223
 
            'power_pass': password,
224
 
            'blade_id': blade_id,
225
 
        }
226
 
        system_id = create_node(macs, 'amd64', 'msftocs', params).wait(30)
227
 
 
228
 
        if accept_all:
229
 
            commission_node(system_id, user).wait(30)