1
1
# Copyright 2012 Canonical Ltd. This software is licensed under the
2
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""MaaS API client for Juju"""
4
"""MAAS API client for Juju"""
6
6
from base64 import b64encode
8
9
from urllib import urlencode
10
from urlparse import urljoin
12
from juju.errors import ProviderError
10
13
from juju.providers.common.utils import convert_unknown_error
11
from juju.providers.maas.auth import MaaSOAuthConnection
14
from juju.providers.maas.auth import MAASOAuthConnection
12
15
from juju.providers.maas.files import encode_multipart_data
15
18
CONSUMER_SECRET = ""
18
class MaaSClient(MaaSOAuthConnection):
21
_re_resource_uri = re.compile(
22
'/api/(?P<version>[^/]+)/nodes/(?P<system_id>[^/]+)/?')
25
def extract_system_id(resource_uri):
26
"""Extract a system ID from a resource URI.
28
This is fairly unforgiving; an exception is raised if the URI given does
29
not look like a MAAS node resource URI.
31
:param resource_uri: A URI that corresponds to a MAAS node resource.
32
:raises: :exc:`juju.errors.ProviderError` when `resource_uri` does not
33
resemble a MAAS node resource URI.
35
match = _re_resource_uri.search(resource_uri)
38
"%r does not resemble a MAAS resource URI." % (resource_uri,))
40
return match.group("system_id")
43
class MAASClient(MAASOAuthConnection):
20
45
def __init__(self, config):
21
"""Initialise an API client for MaaS.
46
"""Initialise an API client for MAAS.
23
48
:param config: a dict of configuration values; must contain
24
49
'maas-server', 'maas-oauth', 'admin-secret'
26
51
self.url = config["maas-server"]
27
if self.url.endswith('/'):
28
# Remove the trailing slash as MaaS provides resource uris
29
# with it included at the start and then goes bang if you
30
# give it two slashes.
31
self.url = self.url[:-1]
52
if not self.url.endswith('/'):
32
54
self.oauth_info = config["maas-oauth"]
33
55
self.admin_secret = config["admin-secret"]
34
super(MaaSClient, self).__init__(self.oauth_info)
36
def _get(self, path, params):
37
"""Dispatch a C{GET} call to a MaaS server.
39
:param uri: The MaaS path for the endpoint to call.
56
super(MAASClient, self).__init__(self.oauth_info)
58
def get(self, path, params):
59
"""Dispatch a C{GET} call to a MAAS server.
61
:param uri: The MAAS path for the endpoint to call.
40
62
:param params: A C{dict} of parameters - or sequence of 2-tuples - to
41
63
encode into the request.
42
64
:return: A Deferred which fires with the result of the call.
44
url = "%s%s?%s" % (self.url, path, urlencode(params))
66
url = "%s?%s" % (urljoin(self.url, path), urlencode(params))
45
67
d = self.dispatch_query(url)
46
68
d.addCallback(json.loads)
47
69
d.addErrback(convert_unknown_error)
50
def _post(self, path, params):
51
"""Dispatch a C{POST} call to a MaaS server.
72
def post(self, path, params):
73
"""Dispatch a C{POST} call to a MAAS server.
53
:param uri: The MaaS path for the endpoint to call.
75
:param uri: The MAAS path for the endpoint to call.
54
76
:param params: A C{dict} of parameters to encode into the request.
55
77
:return: A Deferred which fires with the result of the call.
79
url = urljoin(self.url, path)
58
80
body, headers = encode_multipart_data(params, {})
59
81
d = self.dispatch_query(url, "POST", headers=headers, data=body)
60
82
d.addCallback(json.loads)
61
83
d.addErrback(convert_unknown_error)
64
def get_nodes(self, system_ids=None):
65
"""Ask MaaS to return a list of all the nodes it knows about.
86
def get_nodes(self, resource_uris=None):
87
"""Ask MAAS to return a list of all the nodes it knows about.
89
:param resource_uris: The MAAS URIs for the nodes you want to get.
67
90
:return: A Deferred whose value is the list of nodes.
69
params = [("op", "list")]
70
if system_ids is not None:
71
params.extend(("id", system_id) for system_id in system_ids)
72
return self._get("/api/1.0/nodes/", params)
92
params = [("op", "list_allocated")]
93
if resource_uris is not None:
95
("id", extract_system_id(resource_uri))
96
for resource_uri in resource_uris)
97
return self.get("api/1.0/nodes/", params)
74
def acquire_node(self):
75
"""Ask MaaS to assign a node to us.
99
def acquire_node(self, constraints=None):
100
"""Ask MAAS to assign a node to us.
77
102
:return: A Deferred whose value is the resource URI to the node
78
103
that was acquired.
80
105
params = {"op": "acquire"}
81
return self._post("/api/1.0/nodes/", params)
106
if constraints is not None:
107
name = constraints["maas-name"]
109
params["name"] = name
110
return self.post("api/1.0/nodes/", params)
83
112
def start_node(self, resource_uri, user_data):
84
"""Ask MaaS to start a node.
113
"""Ask MAAS to start a node.
86
:param resource_uri: The MaaS URI for the node you want to start.
87
:param user_data: Any blob of data to be passed to MaaS. Must be
115
:param resource_uri: The MAAS URI for the node you want to start.
116
:param user_data: Any blob of data to be passed to MAAS. Must be
88
117
possible to encode as base64.
89
118
:return: A Deferred whose value is the resource data for the node
90
119
as returned by get_nodes().
92
121
assert isinstance(user_data, str), (
93
122
"User data must be a byte string.")
94
123
params = {"op": "start", "user_data": b64encode(user_data)}
95
return self._post(resource_uri, params)
124
return self.post(resource_uri, params)
97
126
def stop_node(self, resource_uri):
98
127
"""Ask maas to shut down a node.
100
:param resource_uri: The MaaS URI for the node you want to stop.
129
:param resource_uri: The MAAS URI for the node you want to stop.
101
130
:return: A Deferred whose value is the resource data for the node
102
131
as returned by get_nodes().
104
133
params = {"op": "stop"}
105
return self._post(resource_uri, params)
134
return self.post(resource_uri, params)
107
136
def release_node(self, resource_uri):
108
"""Ask MaaS to release a node from our ownership.
137
"""Ask MAAS to release a node from our ownership.
110
:param resource_uri: The URI in MaaS for the node you want to release.
139
:param resource_uri: The URI in MAAS for the node you want to release.
111
140
:return: A Deferred which fires with the resource data for the node
114
143
params = {"op": "release"}
115
return self._post(resource_uri, params)
144
return self.post(resource_uri, params)