~lutostag/ubuntu/trusty/maas/1.5.2+packagefix

« back to all changes in this revision

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

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez
  • Date: 2014-03-28 10:43:53 UTC
  • mto: This revision was merged to the branch mainline in revision 57.
  • Revision ID: package-import@ubuntu.com-20140328104353-ekpolg0pm5xnvq2s
Tags: upstream-1.5+bzr2204
ImportĀ upstreamĀ versionĀ 1.5+bzr2204

Show diffs side-by-side

added added

removed removed

Lines of Context:
10
10
str = None
11
11
 
12
12
__metaclass__ = type
 
13
__all__ = [
 
14
    'power_control_seamicro15k_v09',
 
15
    'power_control_seamicro15k_v2',
 
16
    'probe_seamicro15k_and_enlist',
 
17
    ]
13
18
 
14
19
import httplib
15
20
import json
16
21
import logging
17
 
from StringIO import StringIO
 
22
import time
18
23
import urllib2
19
24
import urlparse
20
25
 
21
 
from provisioningserver.custom_hardware.utils import create_node
 
26
import provisioningserver.custom_hardware.utils as utils
 
27
from seamicroclient import (
 
28
    client as seamicro_client,
 
29
    exceptions as seamicro_exceptions,
 
30
    )
22
31
 
23
32
 
24
33
logger = logging.getLogger(__name__)
25
34
 
26
35
 
27
 
class SeaMicroAPIError(Exception):
 
36
class POWER_STATUS:
 
37
    ON = 'Power-On'
 
38
    OFF = 'Power-Off'
 
39
    RESET = 'Reset'
 
40
 
 
41
 
 
42
class SeaMicroError(Exception):
28
43
    """Failure talking to a SeaMicro chassis controller. """
29
 
 
30
 
 
31
 
class SeaMicroAPI(object):
32
 
    def __init__(self, url, username=None, password=None):
 
44
    pass
 
45
 
 
46
 
 
47
class SeaMicroAPIV09Error(SeaMicroError):
 
48
    """Failure talking to a SeaMicro API v0.9. """
 
49
 
 
50
    def __init__(self, msg, response_code=None):
 
51
        super(SeaMicroAPIV09Error, self).__init__(msg)
 
52
        self.response_code = response_code
 
53
 
 
54
 
 
55
class SeaMicroAPIV09(object):
 
56
    allowed_codes = [httplib.OK, httplib.ACCEPTED, httplib.NOT_MODIFIED]
 
57
 
 
58
    def __init__(self, url):
33
59
        """
34
60
        :param url: The URL of the seamicro chassis, e.g.: http://seamciro/v0.9
35
61
        :type url: string
36
62
        """
37
63
        self.url = url
38
 
        self.token = self._get("login", [username, password])
39
 
 
40
 
    def _get(self, location, params=None):
41
 
        """Dispatch a GET request to a SeaMicro chassis.
42
 
 
43
 
        The seamicro box has order-dependent HTTP parameters, so we build
44
 
        our own get URL, and use a list vs. a dict for data, as the order is
45
 
        implicit.
 
64
        self.token = None
 
65
 
 
66
    def build_url(self, location, params=None):
 
67
        """Builds an order-dependent url, as the SeaMicro chassis
 
68
        requires order-dependent parameters.
46
69
        """
47
70
        if params is None:
48
71
            params = []
49
72
        params = filter(None, params)
50
 
 
51
 
        allowed_codes = [httplib.OK, httplib.ACCEPTED, httplib.NOT_MODIFIED]
52
 
 
53
 
        url = urlparse.urljoin(self.url, location) + '?' + '&'.join(params)
54
 
        response = urllib2.urlopen(url)
55
 
        if response.getcode() not in allowed_codes:
56
 
            raise SeaMicroAPIError("got response code %d" % response.getcode())
 
73
        return urlparse.urljoin(self.url, location) + '?' + '&'.join(params)
 
74
 
 
75
    def parse_response(self, url, response):
 
76
        """Parses the HTTP response, checking for errors
 
77
        from the SeaMicro chassis.
 
78
        """
 
79
        if response.getcode() not in self.allowed_codes:
 
80
            raise SeaMicroAPIV09Error(
 
81
                "got response code %s" % response.getcode(),
 
82
                response_code=response.getcode())
57
83
        text = response.read()
58
 
        json_data = json.load(StringIO(text))
 
84
 
 
85
        # Decode the response, it should be json. If not
 
86
        # handle that case and set json_data to None, so
 
87
        # a SeaMicroAPIV09Error can be raised.
 
88
        try:
 
89
            json_data = json.loads(text)
 
90
        except ValueError:
 
91
            json_data = None
59
92
 
60
93
        if not json_data:
61
 
            raise SeaMicroAPIError(
 
94
            raise SeaMicroAPIV09Error(
62
95
                'No JSON data found from %s: got %s' % (url, text))
63
96
        json_rpc_code = int(json_data['error']['code'])
64
 
        if json_rpc_code not in allowed_codes:
65
 
            raise SeaMicroAPIError(
 
97
        if json_rpc_code not in self.allowed_codes:
 
98
            raise SeaMicroAPIV09Error(
66
99
                'Got JSON RPC error code %d: %s for %s' % (
67
100
                    json_rpc_code,
68
101
                    httplib.responses.get(json_rpc_code, 'Unknown!'),
69
 
                    url))
70
 
 
71
 
        return json_data['result']
 
102
                    url),
 
103
                response_code=json_rpc_code)
 
104
        return json_data
 
105
 
 
106
    def get(self, location, params=None):
 
107
        """Dispatch a GET request to a SeaMicro chassis.
 
108
 
 
109
        The seamicro box has order-dependent HTTP parameters, so we build
 
110
        our own get URL, and use a list vs. a dict for data, as the order is
 
111
        implicit.
 
112
        """
 
113
        url = self.build_url(location, params)
 
114
        response = urllib2.urlopen(url)
 
115
        json_data = self.parse_response(url, response)
 
116
 
 
117
        return json_data['result']
 
118
 
 
119
    def put(self, location, params=None):
 
120
        """Dispatch a PUT request to a SeaMicro chassis.
 
121
 
 
122
        The seamicro box has order-dependent HTTP parameters, so we build
 
123
        our own get URL, and use a list vs. a dict for data, as the order is
 
124
        implicit.
 
125
        """
 
126
        opener = urllib2.build_opener(urllib2.HTTPHandler)
 
127
        url = self.build_url(location, params)
 
128
        request = urllib2.Request(url)
 
129
        request.get_method = lambda: 'PUT'
 
130
        request.add_header('content-type', 'text/json')
 
131
        response = opener.open(request)
 
132
        json_data = self.parse_response(url, response)
 
133
 
 
134
        return json_data['result']
 
135
 
 
136
    def is_logged_in(self):
 
137
        return self.token is not None
 
138
 
 
139
    def login(self, username, password):
 
140
        if not self.is_logged_in():
 
141
            self.token = self.get("login", [username, password])
 
142
 
 
143
    def logout(self):
 
144
        if self.is_logged_in():
 
145
            self.get("logout")
 
146
            self.token = None
72
147
 
73
148
    def servers_all(self):
74
 
        return self._get("servers/all", [self.token])
75
 
 
76
 
 
77
 
def probe_seamicro15k_and_enlist(ip, username, password):
78
 
    api = SeaMicroAPI('http://%s/v0.9/' % ip, username, password)
79
 
 
80
 
    servers = (
81
 
        server for _, server in
82
 
        api.servers_all().iteritems()
83
 
        # There are 8 network cards attached to these boxes, we only use NIC 0
84
 
        # for PXE booting.
85
 
        if server['serverNIC'] != '0'
86
 
    )
87
 
 
88
 
    for server in servers:
89
 
        # serverId looks like 63/2, i.e. power id 63, CPU 2. We only want the
90
 
        # system id part of this.
91
 
        [_, system_id] = server['serverId'].split('/')
92
 
        mac = server['serverMacAddr']
93
 
 
 
149
        return self.get("servers/all", [self.token])
 
150
 
 
151
    def servers(self):
 
152
        return self.get("servers", [self.token])
 
153
 
 
154
    def server_index(self, server_id):
 
155
        """API v0.9 uses arbitrary indexing, this function converts a server
 
156
        id to an index that can be used for detailed outputs & commands.
 
157
        """
 
158
        servers = self.servers()['serverId']
 
159
        for idx, name in servers.items():
 
160
            if name == server_id:
 
161
                return idx
 
162
        return None
 
163
 
 
164
    def power_server(self, server_id, new_status, do_pxe=False, force=False):
 
165
        idx = self.server_index(server_id)
 
166
        if idx is None:
 
167
            raise SeaMicroAPIV09Error(
 
168
                'Failed to retrieve server index, '
 
169
                'invalid server_id: %s' % server_id)
 
170
 
 
171
        location = 'servers/%s' % idx
 
172
        params = ['action=%s' % new_status]
 
173
        if new_status in [POWER_STATUS.ON, POWER_STATUS.RESET]:
 
174
            if do_pxe:
 
175
                params.append("using-pxe=true")
 
176
            else:
 
177
                params.append("using-pxe=false")
 
178
        elif new_status in [POWER_STATUS.OFF]:
 
179
            if force:
 
180
                params.append("force=true")
 
181
            else:
 
182
                params.append("force=false")
 
183
        else:
 
184
            raise SeaMicroAPIV09Error('Invalid power action: %s' % new_status)
 
185
 
 
186
        params.append(self.token)
 
187
        self.put(location, params=params)
 
188
        return True
 
189
 
 
190
    def power_on(self, server_id, do_pxe=False):
 
191
        return self.power_server(server_id, POWER_STATUS.ON, do_pxe=do_pxe)
 
192
 
 
193
    def power_off(self, server_id, force=False):
 
194
        return self.power_server(server_id, POWER_STATUS.OFF, force=force)
 
195
 
 
196
    def reset(self, server_id, do_pxe=False):
 
197
        return self.power_server(server_id, POWER_STATUS.RESET, do_pxe=do_pxe)
 
198
 
 
199
 
 
200
def get_seamicro15k_api(version, ip, username, password):
 
201
    """Gets the api client depending on the version.
 
202
    Supports v0.9 and v2.0.
 
203
 
 
204
    :returns: api for version, None if version not supported
 
205
    """
 
206
    if version == 'v0.9':
 
207
        api = SeaMicroAPIV09('http://%s/v0.9/' % ip)
 
208
        try:
 
209
            api.login(username, password)
 
210
        except urllib2.URLError:
 
211
            # Cannot reach using v0.9, might not be supported
 
212
            return None
 
213
        return api
 
214
    elif version == 'v2.0':
 
215
        url = 'http://%s' % ip
 
216
        try:
 
217
            api = seamicro_client.Client(
 
218
                '2', auth_url=url, username=username, password=password)
 
219
        except seamicro_exceptions.ConnectionRefused:
 
220
            # Cannot reach using v2.0, might no be supported
 
221
            return None
 
222
        return api
 
223
 
 
224
 
 
225
def get_seamicro15k_servers(version, ip, username, password):
 
226
    """Gets a list of tuples containing (server_id, mac_address) from the
 
227
    sm15k api version. Supports v0.9 and v2.0.
 
228
 
 
229
    :returns: list of (server_id, mac_address), None if version not supported
 
230
    """
 
231
    api = get_seamicro15k_api(version, ip, username, password)
 
232
    if api:
 
233
        if version == 'v0.9':
 
234
            return (
 
235
                (server['serverId'].split('/')[0], server['serverMacAddr'])
 
236
                for server in
 
237
                api.servers_all().values()
 
238
                # There are 8 network cards attached to these boxes, we only
 
239
                # use NIC 0 for PXE booting.
 
240
                if server['serverNIC'] == '0'
 
241
            )
 
242
        elif version == 'v2.0':
 
243
            return (
 
244
                (server.id, server.serverMacAddr)
 
245
                for server in
 
246
                api.servers.list()
 
247
                # There are 8 network cards attached to these boxes, we only
 
248
                # use NIC 0 for PXE booting.
 
249
                if server.serverNIC == '0'
 
250
            )
 
251
    return None
 
252
 
 
253
 
 
254
def select_seamicro15k_api_version(power_control):
 
255
    """Returns the lastest api version to use."""
 
256
    if power_control == 'ipmi':
 
257
        return ['v2.0', 'v0.9']
 
258
    if power_control == 'restapi':
 
259
        return ['v0.9']
 
260
    if power_control == 'restapi2':
 
261
        return ['v2.0']
 
262
    raise SeaMicroError(
 
263
        'Unsupported power control method: %s.' % power_control)
 
264
 
 
265
 
 
266
def find_seamicro15k_servers(ip, username, password, power_control):
 
267
    """Returns the list of servers, using the latest supported api version."""
 
268
    api_versions = select_seamicro15k_api_version(power_control)
 
269
    for version in api_versions:
 
270
        servers = get_seamicro15k_servers(version, ip, username, password)
 
271
        if servers is not None:
 
272
            return servers
 
273
    raise SeaMicroError('Failure to retrieve servers.')
 
274
 
 
275
 
 
276
def probe_seamicro15k_and_enlist(ip, username, password, power_control=None):
 
277
    power_control = power_control or 'ipmi'
 
278
 
 
279
    servers = find_seamicro15k_servers(ip, username, password, power_control)
 
280
    for system_id, mac in servers:
94
281
        params = {
95
282
            'power_address': ip,
96
283
            'power_user': username,
97
284
            'power_pass': password,
 
285
            'power_control': power_control,
98
286
            'system_id': system_id
99
287
        }
100
288
 
101
 
        create_node(mac, 'amd64', 'sm15k', params)
 
289
        utils.create_node(mac, 'amd64', 'sm15k', params)
 
290
 
 
291
 
 
292
def power_control_seamicro15k_v09(ip, username, password, server_id,
 
293
                                  power_change, retry_count=5, retry_wait=1):
 
294
    server_id = '%s/0' % server_id
 
295
    api = SeaMicroAPIV09('http://%s/v0.9/' % ip)
 
296
 
 
297
    while retry_count > 0:
 
298
        api.login(username, password)
 
299
        try:
 
300
            if power_change == "on":
 
301
                api.power_on(server_id, do_pxe=True)
 
302
            elif power_change == "off":
 
303
                api.power_off(server_id, force=True)
 
304
        except SeaMicroAPIV09Error as e:
 
305
            # Chance that multiple login's are at once, the api
 
306
            # only supports one at a time. So lets try again after
 
307
            # a second, up to max retry count.
 
308
            if e.response_code == 401:
 
309
                retry_count -= 1
 
310
                time.sleep(retry_wait)
 
311
                continue
 
312
            else:
 
313
                raise
 
314
        break
 
315
 
 
316
 
 
317
def power_control_seamicro15k_v2(ip, username, password, server_id,
 
318
                                 power_change):
 
319
    api = get_seamicro15k_api('v2.0', ip, username, password)
 
320
    if api:
 
321
        server = api.servers.get(server_id)
 
322
        if power_change == "on":
 
323
            server.power_on()
 
324
        elif power_change == "off":
 
325
            server.power_off()