Merge branch 'master' into erp5-component-merge
[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 class Recipe(object):
37 """
38 Request a partition to a slap master.
39 Can provide parameters to that partition and fetches its connection
40 parameters.
41
42 Input:
43 server-url
44 key-file (optional)
45 cert-file (optional)
46 Used to contact slap master.
47
48 computer-id
49 partition-id
50 Current partition's identifiers.
51 Must match key's credentials if given.
52
53 name (optional, defaults to section name)
54 Name (reference) of requested partition.
55
56 software-url
57 URL of a software definition to request an instance of.
58
59 software-type
60 Software type of requested instance, among those provided by the
61 definition from software-url.
62
63 slave (optional, defaults to false)
64 Set to "true" when requesting a slave instance, ie just setting a set of
65 parameters in an existing instance.
66
67 sla (optional)
68 Whitespace-separated list of Service Level Agreement names.
69 Each name must correspond to a "sla-<name>" option.
70 Used to specify what a suitable partition would be.
71 Possible names depend on master's capabilities.
72
73 config (optional)
74 Whitespace-separated list of partition parameter names.
75 Each name must correspond to a "config-<name>" option.
76 Possible names depend on requested partition's software type.
77
78 return (optional)
79 Whitespace-separated list of expected partition-published value names.
80 Options will be created from them, in the form of "connection-<name>"
81 As long as requested partition doesn't publish all those values,
82 installation of request section will fail.
83 Possible names depend on requested partition's software type.
84
85 Output:
86 See "return" input key.
87 """
88 failed = None
89
90 def __init__(self, buildout, name, options):
91 self.logger = logging.getLogger(name)
92
93 slap = slapmodule.slap()
94
95 software_url = options['software-url']
96 name = options['name']
97
98 slap.initializeConnection(options['server-url'],
99 options.get('key-file'),
100 options.get('cert-file'),
101 )
102 request = slap.registerComputerPartition(
103 options['computer-id'], options['partition-id']).request
104
105 return_parameters = []
106 if 'return' in options:
107 return_parameters = [str(parameter).strip()
108 for parameter in options['return'].split()]
109 else:
110 self.logger.debug("No parameter to return to main instance."
111 "Be careful about that...")
112
113 software_type = options.get('software-type', DEFAULT_SOFTWARE_TYPE)
114
115 filter_kw = {}
116 if 'sla' in options:
117 for sla_parameter in options['sla'].split():
118 filter_kw[sla_parameter] = options['sla-%s' % sla_parameter]
119
120 partition_parameter_kw = {}
121 if 'config' in options:
122 for config_parameter in options['config'].split():
123 partition_parameter_kw[config_parameter] = \
124 options['config-%s' % config_parameter]
125 partition_parameter_kw = self._filterForStorage(partition_parameter_kw)
126
127 isSlave = options.get('slave', '').lower() in \
128 librecipe.GenericBaseRecipe.TRUE_VALUES
129
130 self._raise_request_exception = None
131 self._raise_request_exception_formatted = None
132 self.instance = None
133 try:
134 self.instance = request(software_url, software_type,
135 name, partition_parameter_kw=partition_parameter_kw,
136 filter_kw=filter_kw, shared=isSlave)
137 return_parameter_dict = self._getReturnParameterDict(self.instance,
138 return_parameters)
139 # XXX what is the right way to get a global id?
140 options['instance_guid'] = self.instance.getId()
141 except (slapmodule.NotFoundError, slapmodule.ServerError, slapmodule.ResourceNotReady) as exc:
142 self._raise_request_exception = exc
143 self._raise_request_exception_formatted = traceback.format_exc()
144
145 for param in return_parameters:
146 options['connection-%s' % param] = ''
147 if not self.instance:
148 continue
149 try:
150 value = return_parameter_dict[param]
151 except KeyError:
152 value = ''
153 except (slapmodule.NotFoundError, slapmodule.ServerError, slapmodule.ResourceNotReady):
154 if self.failed is None:
155 self.failed = param
156 options['connection-%s' % param] = value
157
158 def _filterForStorage(self, partition_parameter_kw):
159 return partition_parameter_kw
160
161 def _getReturnParameterDict(self, instance, return_parameter_list):
162 result = {}
163 for param in return_parameter_list:
164 try:
165 result[param] = str(instance.getConnectionParameter(param))
166 except slapmodule.NotFoundError:
167 pass
168 return result
169
170 def install(self):
171 if self._raise_request_exception:
172 raise self._raise_request_exception
173
174 if self.failed is not None:
175 # Check instance status to know if instance has been deployed
176 try:
177 if self.instance._computer_id is not None:
178 status = self.instance.getState()
179 else:
180 status = 'not ready yet'
181 except (slapmodule.NotFoundError, slapmodule.ServerError, slapmodule.ResourceNotReady):
182 status = 'not ready yet'
183 except AttributeError:
184 status = 'unknown'
185 error_message = 'Connection parameter %s not found. '\
186 'Status of requested instance is: %s. If this error persists, '\
187 'check status of this instance.' % (self.failed, status)
188 self.logger.error(error_message)
189 raise KeyError(error_message)
190 return []
191
192 update = install
193
194
195 class RequestOptional(Recipe):
196 """
197 Request a SlapOS instance. Won't fail if request failed or is not ready.
198 Same as slapos.cookbook:request, but won't raise in case of problem.
199 """
200 def install(self):
201 if self._raise_request_exception_formatted:
202 self.logger.warning('Optional request failed.')
203 if not isinstance(self._raise_request_exception, slapmodule.NotFoundError):
204 # full traceback for optional 'not found' is too verbose and confusing
205 self.logger.warning(self._raise_request_exception_formatted)
206 elif self.failed is not None:
207 # Check instance status to know if instance has been deployed
208 try:
209 if self.instance._computer_id is not None:
210 status = self.instance.getState()
211 else:
212 status = 'not ready yet'
213 except (slapmodule.NotFoundError, slapmodule.ServerError):
214 status = 'not ready yet'
215 except AttributeError:
216 status = 'unknown'
217 error_message = 'Connection parameter %s not found. '\
218 'Requested instance is currently %s. If this error persists, '\
219 'check status of this instance.' % (self.failed, status)
220 self.logger.warning(error_message)
221 return []
222
223 update = install
224
225
226 class Serialised(Recipe):
227 def _filterForStorage(self, partition_parameter_kw):
228 return wrap(partition_parameter_kw)
229
230 def _getReturnParameterDict(self, instance, return_parameter_list):
231 try:
232 return json.loads(instance.getConnectionParameter(JSON_SERIALISED_MAGIC_KEY))
233 except slapmodule.NotFoundError:
234 return {}