OLD | NEW |
| (Empty) |
1 from base64 import b64encode | |
2 from xmlrpclib import Fault | |
3 | |
4 from twisted.internet.defer import inlineCallbacks, returnValue | |
5 from twisted.web.xmlrpc import Proxy | |
6 | |
7 from juju.errors import MachinesNotFound, ProviderError | |
8 | |
9 | |
10 def _get_arch(system): | |
11 """Try to parse the system's profile field. | |
12 | |
13 Depends on the profile naming as set up on orchestra install. | |
14 """ | |
15 parts = system.get("profile", "").split("-") | |
16 if len(parts) >= 2: | |
17 if parts[1] in ("x86_64", "i386"): | |
18 return parts[1] | |
19 | |
20 | |
21 def _get_profile(series, arch): | |
22 """Construct an appropriate profile for a system. | |
23 | |
24 Depends on the profile naming as set up on orchestra install. | |
25 """ | |
26 return "%s-%s-juju" % (series, arch) | |
27 | |
28 | |
29 class CobblerCaller(object): | |
30 """Handles the details of communicating with a Cobbler server""" | |
31 | |
32 def __init__(self, config): | |
33 self._user = config["orchestra-user"] | |
34 self._pass = config["orchestra-pass"] | |
35 self._token = "" | |
36 self._proxy = Proxy("http://%(orchestra-server)s/cobbler_api" % config) | |
37 | |
38 def _login(self): | |
39 login = self.call("login", (self._user, self._pass)) | |
40 login.addCallback(self._set_token) | |
41 | |
42 def bad_credentials(failure): | |
43 failure.trap(Fault) | |
44 if "login failed" not in failure.getErrorMessage(): | |
45 return failure | |
46 raise ProviderError("Cobbler server rejected credentials.") | |
47 login.addErrback(bad_credentials) | |
48 return login | |
49 | |
50 def _set_token(self, token): | |
51 self._token = token | |
52 | |
53 def call(self, name, args=(), auth=False): | |
54 | |
55 def call(): | |
56 call_args = args | |
57 if auth: | |
58 call_args += (self._token,) | |
59 return self._proxy.callRemote(name, *call_args) | |
60 | |
61 def login_retry(failure): | |
62 # Login tokens expire after an hour: it seems more sensible | |
63 # to assume we always have a valid one, and to relogin and | |
64 # retry if it fails, than to try to maintain validity state. | |
65 # NOTE: some methods, such as get_system_handle, expect | |
66 # tokens but appear not to check validity. | |
67 failure.trap(Fault) | |
68 if "invalid token" not in failure.getErrorMessage(): | |
69 return failure | |
70 login = self._login() | |
71 login.addCallback(lambda unused: call()) | |
72 return login | |
73 | |
74 result = call() | |
75 if auth: | |
76 result.addErrback(login_retry) | |
77 return result | |
78 | |
79 def check_call(self, name, args=(), auth=False, expect=None): | |
80 | |
81 def check(actual): | |
82 if actual != expect: | |
83 raise ProviderError( | |
84 "Bad result from call to %s with %s: got %r, expected %r" | |
85 % (name, args, actual, expect)) | |
86 return actual | |
87 | |
88 call = self.call(name, args, auth=auth) | |
89 call.addCallback(check) | |
90 return call | |
91 | |
92 | |
93 class CobblerClient(object): | |
94 """Convenient interface to a Cobbler server""" | |
95 | |
96 def __init__(self, config): | |
97 self._caller = CobblerCaller(config) | |
98 self._acquired_class = config["acquired-mgmt-class"] | |
99 self._available_class = config["available-mgmt-class"] | |
100 | |
101 def _get_name(self, instance_id): | |
102 d = self._caller.call("find_system", ({"uid": instance_id},)) | |
103 | |
104 def extract_name(systems): | |
105 if len(systems) > 1: | |
106 raise ProviderError( | |
107 "Got multiple names for machine %s: %s" | |
108 % (instance_id, ", ".join(systems))) | |
109 if not systems: | |
110 raise MachinesNotFound([instance_id]) | |
111 return systems[0] | |
112 d.addCallback(extract_name) | |
113 return d | |
114 | |
115 def _power_call(self, operation, names): | |
116 # note: cobbler immediately returns something looking like a timestamp | |
117 # that we don't know how to interpret; we can't tell if this fails. | |
118 return self._caller.call("background_power_system", | |
119 ({"power": operation, "systems": names},), | |
120 auth=True) | |
121 | |
122 def _class_swapper(self, class_): | |
123 if class_ == self._available_class: | |
124 return self._acquired_class | |
125 if class_ == self._acquired_class: | |
126 return self._available_class | |
127 return class_ | |
128 | |
129 @inlineCallbacks | |
130 def _get_available_system(self, required_mgmt_classes): | |
131 mgmt_classes = [self._available_class] | |
132 if required_mgmt_classes: | |
133 mgmt_classes.extend(required_mgmt_classes) | |
134 names = yield self._caller.call( | |
135 "find_system", ({ | |
136 "mgmt_classes": " ".join(mgmt_classes), | |
137 "netboot_enabled": "true"},)) | |
138 if not names: | |
139 raise ProviderError( | |
140 "Could not find a suitable Cobbler system (set to netboot, " | |
141 "and a member of the following management classes: %s)" | |
142 % ", ".join(mgmt_classes)) | |
143 | |
144 # It's possible that some systems could be marked both available and | |
145 # acquired, so we check each one for sanity (but only complain when | |
146 # the problem becomes critical: ie, we can't find a system in a sane | |
147 # state). | |
148 inconsistent_instance_ids = [] | |
149 for name in names: | |
150 info = yield self._caller.call("get_system", (name,)) | |
151 if info == "~": | |
152 continue | |
153 if _get_arch(info) is None: | |
154 # We can't tell how to set a profile to match the hardware. | |
155 continue | |
156 classes = info["mgmt_classes"] | |
157 if self._acquired_class in classes: | |
158 inconsistent_instance_ids.append(info["uid"]) | |
159 continue | |
160 returnValue((info["uid"], map(self._class_swapper, classes))) | |
161 if inconsistent_instance_ids: | |
162 raise ProviderError( | |
163 "All available Cobbler systems were also marked as acquired " | |
164 "(instances: %s)." | |
165 % ", ".join(inconsistent_instance_ids)) | |
166 raise ProviderError( | |
167 "No available Cobbler system had a detectable known architecture.") | |
168 | |
169 @inlineCallbacks | |
170 def _update_system(self, instance_id, info): | |
171 """Set an attribute on a Cobbler system. | |
172 | |
173 :param str instance_id: the Cobbler uid of the system | |
174 | |
175 :param dict info: names and desired values of system attributes to set | |
176 | |
177 :raises: :exc:`juju.errors.ProviderError` on invalid cobbler state | |
178 :raises: :exc:`juju.errors.MachinesNotFound` when `instance_id` is | |
179 not acquired | |
180 """ | |
181 name = yield self._get_name(instance_id) | |
182 try: | |
183 handle = yield self._caller.call( | |
184 "get_system_handle", (name,), auth=True) | |
185 except Fault as e: | |
186 if "unknown system name" in str(e): | |
187 raise MachinesNotFound([instance_id]) | |
188 raise | |
189 for (key, value) in sorted(info.items()): | |
190 yield self._caller.check_call( | |
191 "modify_system", (handle, key, value), auth=True, expect=True) | |
192 yield self._caller.check_call( | |
193 "save_system", (handle,), auth=True, expect=True) | |
194 returnValue(name) | |
195 | |
196 @inlineCallbacks | |
197 def describe_systems(self, *instance_ids): | |
198 """Get all available information about systems. | |
199 | |
200 :param instance_ids: Cobbler uids of requested systems; leave blank to | |
201 return information about all acquired systems. | |
202 | |
203 :return: a list of dictionaries describing acquired systems | |
204 :rtype: :class:`twisted.internet.defer.Deferred` | |
205 | |
206 :raises: :exc:`juju.errors.MachinesNotFound` if any requested | |
207 instance_id doesn't exist | |
208 """ | |
209 all_systems = yield self._caller.call("get_systems") | |
210 acquired_systems = [s for s in all_systems | |
211 if self._acquired_class in s["mgmt_classes"]] | |
212 if not instance_ids: | |
213 returnValue(acquired_systems) | |
214 | |
215 keyed_systems = dict(((system["uid"], system) | |
216 for system in acquired_systems)) | |
217 result_systems = [] | |
218 missing_instance_ids = [] | |
219 for instance_id in instance_ids: | |
220 if instance_id in keyed_systems: | |
221 result_systems.append(keyed_systems[instance_id]) | |
222 else: | |
223 missing_instance_ids.append(instance_id) | |
224 if missing_instance_ids: | |
225 raise MachinesNotFound(missing_instance_ids) | |
226 returnValue(result_systems) | |
227 | |
228 @inlineCallbacks | |
229 def acquire_system(self, require_classes): | |
230 """Find a system marked as available and mark it as acquired. | |
231 | |
232 :param require_classes: required cobbler mgmt_classes for the machine. | |
233 :type require_classes: list of str | |
234 | |
235 :return: the instance id (Cobbler uid) str of the acquired system. | |
236 :rtype: :class:`twisted.internet.defer.Deferred` | |
237 | |
238 :raises: :exc:`juju.errors.ProviderError` if no suitable system can | |
239 be found. | |
240 """ | |
241 instance_id, new_classes = yield self._get_available_system( | |
242 require_classes) | |
243 yield self._update_system(instance_id, {"mgmt_classes": new_classes}) | |
244 returnValue(instance_id) | |
245 | |
246 @inlineCallbacks | |
247 def start_system(self, instance_id, machine_id, ubuntu_series, user_data): | |
248 """Launch a cobbler system. | |
249 | |
250 :param str instance_id: The Cobbler uid of the desired system. | |
251 | |
252 :param str ks_meta: kickstart metadata with which to launch system. | |
253 | |
254 :return: dict of instance data | |
255 :rtype: :class:`twisted.internet.defer.Deferred` | |
256 """ | |
257 (system,) = yield self.describe_systems(instance_id) | |
258 profile = _get_profile(ubuntu_series, _get_arch(system)) | |
259 ks_meta = system["ks_meta"] | |
260 ks_meta["MACHINE_ID"] = machine_id | |
261 ks_meta["USER_DATA_BASE64"] = b64encode(user_data) | |
262 name = yield self._update_system(instance_id, { | |
263 "netboot_enabled": True, "ks_meta": ks_meta, "profile": profile}) | |
264 yield self._power_call("on", [name]) | |
265 (info,) = yield self.describe_systems(instance_id) | |
266 returnValue(info) | |
267 | |
268 @inlineCallbacks | |
269 def shutdown_system(self, instance_id): | |
270 """Take a system marked as acquired, and make it available again | |
271 | |
272 :rtype: :class:`twisted.internet.defer.Deferred` | |
273 """ | |
274 (system,) = yield self.describe_systems(instance_id) | |
275 ks_meta = system["ks_meta"] | |
276 ks_meta.pop("MACHINE_ID", None) | |
277 ks_meta.pop("USER_DATA_BASE64", None) | |
278 new_classes = map(self._class_swapper, system["mgmt_classes"]) | |
279 name = yield self._update_system( | |
280 instance_id, {"netboot_enabled": True, | |
281 "mgmt_classes": new_classes, | |
282 "ks_meta": ks_meta}) | |
283 yield self._power_call("off", [name]) | |
284 returnValue(True) | |
OLD | NEW |