request: use requester state as default state for the request
[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 # By default
123 requested_state = options.get('state', buildout['slap-connection'].get('requested','started'))
124 slap = slapmodule.slap()
125 slap.initializeConnection(
126 options['server-url'],
127 options.get('key-file'),
128 options.get('cert-file'),
129 )
130 request = slap.registerComputerPartition(
131 options['computer-id'],
132 options['partition-id'],
133 ).request
134 self._raise_request_exception = None
135 self._raise_request_exception_formatted = None
136 self.instance = None
137 # Try to do the request and fetch parameter dict...
138 try:
139 self.instance = request(software_url, software_type,
140 name, partition_parameter_kw=partition_parameter_kw,
141 filter_kw=filter_kw, shared=slave, state=requested_state)
142 return_parameter_dict = self._getReturnParameterDict(self.instance,
143 return_parameters)
144 if not slave:
145 try:
146 options['instance-guid'] = self.instance.getInstanceGuid()
147 # XXX: deprecated, to be removed
148 options['instance_guid'] = self.instance.getInstanceGuid()
149 except (slapmodule.ResourceNotReady, AttributeError):
150 # Backward compatibility. Old SlapOS master and core don't know this.
151 self.logger.warning("Impossible to fetch instance GUID.")
152 except (slapmodule.NotFoundError, slapmodule.ServerError, slapmodule.ResourceNotReady) as exc:
153 self._raise_request_exception = exc
154 self._raise_request_exception_formatted = traceback.format_exc()
155 return_parameter_dict = {}
156
157 # Then try to get all the parameters. In case of problem, put empty string.
158 for param in return_parameters:
159 options['connection-%s' % param] = ''
160 try:
161 options['connection-%s' % param] = return_parameter_dict[param]
162 except KeyError:
163 if self.failed is None:
164 self.failed = param
165 options['requested-state'] = requested_state
166 options['instance-state'] = self.instance.getState()
167
168 def _filterForStorage(self, partition_parameter_kw):
169 return partition_parameter_kw
170
171 def _getReturnParameterDict(self, instance, return_parameter_list):
172 result = {}
173 for param in return_parameter_list:
174 try:
175 result[param] = str(instance.getConnectionParameter(param))
176 except slapmodule.NotFoundError:
177 pass
178 return result
179
180 def install(self):
181 if self._raise_request_exception:
182 raise self._raise_request_exception
183
184 if self.failed is not None:
185 # Check instance status to know if instance has been deployed
186 try:
187 if self.instance._computer_id is not None:
188 status = self.instance.getState()
189 else:
190 status = 'not ready yet'
191 except (slapmodule.NotFoundError, slapmodule.ServerError, slapmodule.ResourceNotReady):
192 status = 'not ready yet'
193 except AttributeError:
194 status = 'unknown'
195 error_message = 'Connection parameter %s not found. '\
196 'Status of requested instance is: %s. If this error persists, '\
197 'check status of this instance.' % (self.failed, status)
198 self.logger.error(error_message)
199 raise KeyError(error_message)
200 return []
201
202 update = install
203
204
205 class RequestOptional(Recipe):
206 """
207 Request a SlapOS instance. Won't fail if request failed or is not ready.
208 Same as slapos.cookbook:request, but won't raise in case of problem.
209 """
210 def install(self):
211 if self._raise_request_exception_formatted:
212 self.logger.warning('Optional request failed.')
213 if not isinstance(self._raise_request_exception, slapmodule.NotFoundError):
214 # full traceback for optional 'not found' is too verbose and confusing
215 self.logger.debug(self._raise_request_exception_formatted)
216 elif self.failed is not None:
217 # Check instance status to know if instance has been deployed
218 try:
219 if self.instance._computer_id is not None:
220 status = self.instance.getState()
221 else:
222 status = 'not ready yet'
223 except (slapmodule.NotFoundError, slapmodule.ServerError, slapmodule.ResourceNotReady):
224 status = 'not ready yet'
225 except AttributeError:
226 status = 'unknown'
227 error_message = 'Connection parameter %s not found. '\
228 'Requested instance is currently %s. If this error persists, '\
229 'check status of this instance.' % (self.failed, status)
230 self.logger.warning(error_message)
231 return []
232
233 update = install
234
235 class Serialised(Recipe):
236 def _filterForStorage(self, partition_parameter_kw):
237 return wrap(partition_parameter_kw)
238
239 def _getReturnParameterDict(self, instance, return_parameter_list):
240 try:
241 return json.loads(instance.getConnectionParameter(JSON_SERIALISED_MAGIC_KEY))
242 except slapmodule.NotFoundError:
243 return {}
244
245
246
247
248 CONNECTION_PARAMETER_STRING = 'connection-'
249
250 class RequestEdge(Recipe):
251 """
252 For each country in country-list, do a request.
253 """
254 def __init__(self, buildout, name, options):
255 self.logger = logging.getLogger(name)
256 self.options = options
257 self.request_dict = {}
258 # Keep a copy of original options dict
259 original_options = options.copy()
260 for country in options['country-list'].split(','):
261 # Request will have its own copy of options dict
262 local_options = original_options.copy()
263 local_options['name'] = '%s-%s' % (country, name)
264 local_options['sla'] = "region"
265 local_options['sla-region'] = country
266
267 self.request_dict[country] = Recipe(buildout, name, local_options)
268 # "Bubble" all connection parameters
269 for option, value in local_options.iteritems():
270 if option.startswith(CONNECTION_PARAMETER_STRING):
271 self.options['%s-%s' % (option, country)] = value
272
273 def install(self):
274 for country, request in self.request_dict.iteritems():
275 request.install()
276 return []
277
278 update = install
279