Fix regression in GenericBaseRecipe.generatePassword
[slapos.git] / slapos / recipe / librecipe / generic.py
1 # -*- coding: utf-8 -*-
2 # vim: set et sts=2:
3 ##############################################################################
4 #
5 # Copyright (c) 2010 Vifib SARL and Contributors. All Rights Reserved.
6 #
7 # WARNING: This program as such is intended to be used by professional
8 # programmers who take the whole responsibility of assessing all potential
9 # consequences resulting from its eventual inadequacies and bugs
10 # End users who are looking for a ready-to-use solution with commercial
11 # guarantees and support are strongly adviced to contract a Free Software
12 # Service Company
13 #
14 # This program is Free Software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; either version 3
17 # of the License, or (at your option) any later version.
18 #
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
27 #
28 ##############################################################################
29 import io
30 import logging
31 import os
32 import sys
33 import inspect
34 import re
35 import shutil
36 import urllib
37 import urlparse
38
39 import pkg_resources
40 import zc.buildout
41
42 from slapos.recipe.librecipe import shlex
43
44 class GenericBaseRecipe(object):
45 """Boilerplate class for all Buildout recipes providing helpful methods like
46 creating configuration file, creating wrappers, generating passwords, etc.
47 Can be extended in SlapOS recipes to ease development.
48 """
49
50 TRUE_VALUES = ['y', 'yes', '1', 'true']
51 FALSE_VALUES = ['n', 'no', '0', 'false']
52
53 def __init__(self, buildout, name, options):
54 """Recipe initialisation"""
55 self.name = name
56 self.buildout = buildout
57 self.logger = logging.getLogger(name)
58
59 self.options = options.copy() # If _options use self.optionIsTrue
60 self._options(options) # Options Hook
61 self.options = options.copy() # Updated options dict
62
63 self._ws = self.getWorkingSet()
64
65 def update(self):
66 """By default update method does the same thing than install"""
67 return self.install()
68
69 def install(self):
70 """Install method of the recipe. This must be overriden in child
71 classes """
72 raise NotImplementedError("install method is not implemented.")
73
74 def getWorkingSet(self):
75 """If you want do override the default working set"""
76 egg = zc.recipe.egg.Egg(self.buildout, 'slapos.cookbook',
77 self.options.copy())
78 requirements, ws = egg.working_set()
79 return ws
80
81 def _options(self, options):
82 """Options Hook method. This method can be overriden in child classes"""
83 return
84
85 def createFile(self, name, content, mode=0600):
86 """Create a file with content
87
88 The parent directory should exists, else it would raise IOError"""
89 with open(name, 'w') as fileobject:
90 fileobject.write(content)
91 os.chmod(fileobject.name, mode)
92 return os.path.abspath(name)
93
94 def createExecutable(self, name, content, mode=0700):
95 return self.createFile(name, content, mode)
96
97 def addLineToFile(self, filepath, line, encoding='utf8'):
98 """Append a single line to a text file, if the line does not exist yet.
99
100 line must be unicode."""
101
102 if os.path.exists(filepath):
103 lines = [l.rstrip('\n') for l in io.open(filepath, 'r', encoding=encoding)]
104 else:
105 lines = []
106
107 if not line in lines:
108 lines.append(line)
109 with io.open(filepath, 'w+', encoding=encoding) as f:
110 f.write(u'\n'.join(lines))
111
112 def createPythonScript(self, name, absolute_function, arguments=''):
113 """Create a python script using zc.buildout.easy_install.scripts
114
115 * function should look like 'module.function', or only 'function'
116 if it is a builtin function."""
117 absolute_function = tuple(absolute_function.rsplit('.', 1))
118 if len(absolute_function) == 1:
119 absolute_function = ('__builtin__',) + absolute_function
120 if len(absolute_function) != 2:
121 raise ValueError("A non valid function was given")
122
123 module, function = absolute_function
124 path, filename = os.path.split(os.path.abspath(name))
125
126 script = zc.buildout.easy_install.scripts(
127 [(filename, module, function)], self._ws, sys.executable,
128 path, arguments=arguments)[0]
129 return script
130
131 def createWrapper(self, name, command, parameters, comments=[],
132 parameters_extra=False, environment=None):
133 """
134 Creates a very simple (one command) shell script for process replacement.
135 Takes care of quoting.
136 """
137
138 lines = [ '#!/bin/sh' ]
139
140 for comment in comments:
141 lines.append('# %s' % comment)
142
143 if environment:
144 for key in environment:
145 lines.append('export %s=%s' % (key, environment[key]))
146
147 lines.append('exec %s' % shlex.quote(command))
148
149 for param in parameters:
150 if len(lines[-1]) < 40:
151 lines[-1] += ' ' + shlex.quote(param)
152 else:
153 lines[-1] += ' \\'
154 lines.append('\t' + shlex.quote(param))
155
156 if parameters_extra:
157 # pass-through further parameters
158 lines[-1] += ' \\'
159 lines.append('\t"$@"')
160
161 content = '\n'.join(lines) + '\n'
162 return self.createFile(name, content, 0700)
163
164 def createDirectory(self, parent, name, mode=0700):
165 path = os.path.join(parent, name)
166 if not os.path.exists(path):
167 os.mkdir(path, mode)
168 elif not os.path.isdir(path):
169 raise OSError("%r exists but is not a directory." % name)
170 return path
171
172 def substituteTemplate(self, template_location, mapping_dict):
173 """Read from file template_location an substitute content with
174 mapping_dict doing a dummy python format."""
175 with open(template_location, 'r') as template:
176 return template.read() % mapping_dict
177
178 def getTemplateFilename(self, template_name):
179 caller = inspect.stack()[1]
180 caller_frame = caller[0]
181 name = caller_frame.f_globals['__name__']
182 return pkg_resources.resource_filename(name,
183 'template/%s' % template_name)
184
185 def generatePassword(self, len_=32):
186 # TODO: Consider having generate.password recipe inherit this class,
187 # so that it can be easily inheritable.
188 # In the long-term, it's probably better that passwords are provided
189 # by software requesters, to avoid keeping unhashed secrets in
190 # partitions when possible.
191 self.logger.warning("GenericBaseRecipe.generatePassword is deprecated."
192 " Use generate.password recipe instead.")
193 return "insecure"
194
195 def isTrueValue(self, value):
196 return str(value).lower() in GenericBaseRecipe.TRUE_VALUES
197
198 def optionIsTrue(self, optionname, default=None):
199 if default is not None and optionname not in self.options:
200 return default
201 return self.isTrueValue(self.options[optionname])
202
203 def unparseUrl(self, scheme, host, path='', params='', query='',
204 fragment='', port=None, auth=None):
205 """Join a url with auth, host, and port.
206
207 * auth can be either a login string or a tuple (login, password).
208 * if the host is an ipv6 address, brackets will be added to surround it.
209
210 """
211 netloc = ''
212 if auth is not None:
213 auth = tuple(auth)
214 netloc = urllib.quote(str(auth[0])) # Login
215 if len(auth) > 1:
216 netloc += ':%s' % urllib.quote(auth[1]) # Password
217 netloc += '@'
218
219 # host is an ipv6 address whithout brackets
220 if ':' in host and not re.match(r'^\[.*\]$', host):
221 netloc += '[%s]' % host
222 else:
223 netloc += str(host)
224
225 if port is not None:
226 netloc += ':%s' % port
227
228 url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
229
230 return url
231
232 def setLocationOption(self):
233 if not self.options.get('location'):
234 self.options['location'] = os.path.join(
235 self.buildout['buildout']['parts-directory'], self.name)
236
237 def download(self, destination=None):
238 """ A simple wrapper around h.r.download, downloading to self.location"""
239 self.setLocationOption()
240
241 import hexagonit.recipe.download
242 if not destination:
243 destination = self.location
244 if os.path.exists(destination):
245 # leftovers from a previous failed attempt, removing it.
246 self.logger.warning('Removing already existing directory %s',
247 destination)
248 shutil.rmtree(destination)
249 os.mkdir(destination)
250
251 try:
252 options = self.options.copy()
253 options['destination'] = destination
254 hexagonit.recipe.download.Recipe(
255 self.buildout, self.name, options).install()
256 except:
257 shutil.rmtree(destination)
258 raise