request: Provide instance status as returned parameter
[slapos.git] / slapos / recipe / request.py
1 ##############################################################################
2 #
3 # Copyright (c) 2010 Vifib SARL and Contributors. All Rights Reserved.
4 #
5 # WARNING: This program as such is intended to be used by professional
6 # programmers who take the whole responsibility of assessing all potential
7 # consequences resulting from its eventual inadequacies and bugs
8 # End users who are looking for a ready-to-use solution with commercial
9 # guarantees and support are strongly adviced to contract a Free Software
10 # Service Company
11 #
12 # This program is Free Software; you can redistribute it and/or
13 # modify it under the terms of the GNU General Public License
14 # as published by the Free Software Foundation; either version 3
15 # of the License, or (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25 #
26 ##############################################################################
27 import logging
28 from slapos.recipe.librecipe import wrap, JSON_SERIALISED_MAGIC_KEY
29 import json
30 from slapos import slap as slapmodule
31 import slapos.recipe.librecipe.generic as librecipe
32 import traceback
33
34 DEFAULT_SOFTWARE_TYPE = 'RootSoftwareInstance'
35
36 def getListOption(option_dict, key, default=()):
37 result = option_dict.get(key, default)
38 if isinstance(result, basestring):
39 result = result.split()
40 return result
41
42 class Recipe(object):
43 """
44 Request a partition to a slap master.
45 Can provide parameters to that partition and fetches its connection
46 parameters.
47
48 Input:
49 server-url
50 key-file (optional)
51 cert-file (optional)
52 Used to contact slap master.
53
54 computer-id
55 partition-id
56 Current partition's identifiers.
57 Must match key's credentials if given.
58
59 name (optional, defaults to section name)
60 Name (reference) of requested partition.
61
62 software-url
63 URL of a software definition to request an instance of.
64
65 software-type
66 Software type of requested instance, among those provided by the
67 definition from software-url.
68
69 slave (optional, defaults to false)
70 Set to "true" when requesting a slave instance, ie just setting a set of
71 parameters in an existing instance.
72
73 sla (optional)
74 Whitespace-separated list of Service Level Agreement names.
75 Each name must correspond to a "sla-<name>" option.
76 Used to specify what a suitable partition would be.
77 Possible names depend on master's capabilities.
78
79 config (optional)
80 Whitespace-separated list of partition parameter names.
81 Each name must correspond to a "config-<name>" option.
82 Possible names depend on requested partition's software type.
83
84 return (optional)
85 Whitespace-separated list of expected partition-published value names.
86 Options will be created from them, in the form of "connection-<name>"
87 As long as requested partition doesn't publish all those values,
88 installation of request section will fail.
89 Possible names depend on requested partition's software type.
90
91 state (optional)
92 Requested state, default value is the state of the requester.
93
94 Output:
95 See "return" input key.
96 "instance-state"
97 The current state of the instance.
98 "requested-state"
99 The requested state of the instance.
100 """
101 failed = None
102
103 def __init__(self, buildout, name, options):
104 self.logger = logging.getLogger(name)
105 software_url = options['software-url']
106 name = options['name']
107 return_parameters = getListOption(options, 'return')
108 if not return_parameters:
109 self.logger.debug("No parameter to return to main instance."
110 "Be careful about that...")
111 software_type = options.get('software-type', DEFAULT_SOFTWARE_TYPE)
112 filter_kw = dict(
113 (x, options['sla-' + x]) for x in getListOption(options, 'sla')
114 if options['sla-' + x]
115 )
116 partition_parameter_kw = self._filterForStorage(dict(
117 (x, options['config-' + x])
118 for x in getListOption(options, 'config')
119 ))
120 slave = options.get('slave', 'false').lower() in \
121 librecipe.GenericBaseRecipe.TRUE_VALUES
122
123 # By default XXXX Way of doing it is ugly and dangerous
124 requested_state = options.get('state', buildout['slap-connection'].get('requested','started'))
125 options['requested-state'] = requested_state
126
127 slap = slapmodule.slap()
128 slap.initializeConnection(
129 options['server-url'],
130 options.get('key-file'),
131 options.get('cert-file'),
132 )
133 request = slap.registerComputerPartition(
134 options['computer-id'],
135 options['partition-id'],
136 ).request
137 self._raise_request_exception = None
138 self._raise_request_exception_formatted = None
139 self.instance = None
140
141 # Try to do the request and fetch parameter dict...
142 try:
143 self.instance = request(software_url, software_type,
144 name, partition_parameter_kw=partition_parameter_kw,
145 filter_kw=filter_kw, shared=slave, state=requested_state)
146 return_parameter_dict = self._getReturnParameterDict(self.instance,
147 return_parameters)
148 # Fetch the instance-guid and the instance-state
149 # Note: SlapOS Master does not support it for slave instances
150 if not slave:
151 try:
152 options['instance-guid'] = self.instance.getInstanceGuid()
153 # XXX: deprecated, to be removed
154 options['instance_guid'] = self.instance.getInstanceGuid()
155 options['instance-state'] = self.instance.getState()
156 options['instance-status'] = self.instance.getStatus()
157 except (slapmodule.ResourceNotReady, AttributeError):
158 # Backward compatibility. Old SlapOS master and core don't know this.
159 self.logger.warning("Impossible to fetch instance GUID nor state.")
160 except (slapmodule.NotFoundError, slapmodule.ServerError, slapmodule.ResourceNotReady) as exc:
161 self._raise_request_exception = exc
162 self._raise_request_exception_formatted = traceback.format_exc()
163 return_parameter_dict = {}
164
165 # Then try to get all the parameters. In case of problem, put empty string.
166 for param in return_parameters:
167 options['connection-%s' % param] = ''
168 try:
169 options['connection-%s' % param] = return_parameter_dict[param]
170 except KeyError:
171 if self.failed is None:
172 self.failed = param
173
174 def _filterForStorage(self, partition_parameter_kw):
175 return partition_parameter_kw
176
177 def _getReturnParameterDict(self, instance, return_parameter_list):
178 result = {}
179 for param in return_parameter_list:
180 try:
181 result[param] = str(instance.getConnectionParameter(param))
182 except slapmodule.NotFoundError:
183 pass
184 return result
185
186 def install(self):
187 if self._raise_request_exception:
188 raise self._raise_request_exception
189
190 if self.failed is not None:
191 # Check instance status to know if instance has been deployed
192 try:
193 if self.instance._computer_id is not None:
194 status = self.instance.getState()
195 else:
196 status = 'not ready yet'
197 except (slapmodule.NotFoundError, slapmodule.ServerError, slapmodule.ResourceNotReady):
198 status = 'not ready yet'
199 except AttributeError:
200 status = 'unknown'
201 error_message = 'Connection parameter %s not found. '\
202 'Status of requested instance is: %s. If this error persists, '\
203 'check status of this instance.' % (self.failed, status)
204 self.logger.error(error_message)
205 raise KeyError(error_message)
206 return []
207
208 update = install
209
210
211 class RequestOptional(Recipe):
212 """
213 Request a SlapOS instance. Won't fail if request failed or is not ready.
214 Same as slapos.cookbook:request, but won't raise in case of problem.
215 """
216 def install(self):
217 if self._raise_request_exception_formatted:
218 self.logger.warning('Optional request failed.')
219 if not isinstance(self._raise_request_exception, slapmodule.NotFoundError):
220 # full traceback for optional 'not found' is too verbose and confusing
221 self.logger.debug(self._raise_request_exception_formatted)
222 elif self.failed is not None:
223 # Check instance status to know if instance has been deployed
224 try:
225 if self.instance._computer_id is not None:
226 status = self.instance.getState()
227 else:
228 status = 'not ready yet'
229 except (slapmodule.NotFoundError, slapmodule.ServerError, slapmodule.ResourceNotReady):
230 status = 'not ready yet'
231 except AttributeError:
232 status = 'unknown'
233 error_message = 'Connection parameter %s not found. '\
234 'Requested instance is currently %s. If this error persists, '\
235 'check status of this instance.' % (self.failed, status)
236 self.logger.warning(error_message)
237 return []
238
239 update = install
240
241 class Serialised(Recipe):
242 def _filterForStorage(self, partition_parameter_kw):
243 return wrap(partition_parameter_kw)
244
245 def _getReturnParameterDict(self, instance, return_parameter_list):
246 try:
247 return json.loads(instance.getConnectionParameter(JSON_SERIALISED_MAGIC_KEY))
248 except slapmodule.NotFoundError:
249 return {}
250
251
252
253
254 CONNECTION_PARAMETER_STRING = 'connection-'
255
256 class RequestEdge(Recipe):
257 """
258 For each country in country-list, do a request.
259 """
260 def __init__(self, buildout, name, options):
261 self.logger = logging.getLogger(name)
262 self.options = options
263 self.request_dict = {}
264 # Keep a copy of original options dict
265 original_options = options.copy()
266 for country in options['country-list'].split(','):
267 # Request will have its own copy of options dict
268 local_options = original_options.copy()
269 local_options['name'] = '%s-%s' % (country, name)
270 local_options['sla'] = "region"
271 local_options['sla-region'] = country
272
273 self.request_dict[country] = Recipe(buildout, name, local_options)
274 # "Bubble" all connection parameters
275 for option, value in local_options.iteritems():
276 if option.startswith(CONNECTION_PARAMETER_STRING):
277 self.options['%s-%s' % (option, country)] = value
278
279 def install(self):
280 for country, request in self.request_dict.iteritems():
281 request.install()
282 return []
283
284 update = install
285