Remove automatic 'location' setup in librecipe.
[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=[], parameters_extra=False):
132 """
133 Creates a very simple (one command) shell script for process replacement.
134 Takes care of quoting.
135 """
136
137 lines = [ '#!/bin/sh' ]
138
139 for comment in comments:
140 lines.append('# %s' % comment)
141
142 lines.append('exec %s' % shlex.quote(command))
143
144 for param in parameters:
145 if len(lines[-1]) < 40:
146 lines[-1] += ' ' + shlex.quote(param)
147 else:
148 lines[-1] += ' \\'
149 lines.append('\t' + shlex.quote(param))
150
151 if parameters_extra:
152 # pass-through further parameters
153 lines[-1] += ' \\'
154 lines.append('\t$@')
155
156 content = '\n'.join(lines) + '\n'
157 return self.createFile(name, content, 0700)
158
159 def createDirectory(self, parent, name, mode=0700):
160 path = os.path.join(parent, name)
161 if not os.path.exists(path):
162 os.mkdir(path, mode)
163 elif not os.path.isdir(path):
164 raise OSError("%r exists but is not a directory." % name)
165 return path
166
167 def substituteTemplate(self, template_location, mapping_dict):
168 """Read from file template_location an substitute content with
169 mapping_dict doing a dummy python format."""
170 with open(template_location, 'r') as template:
171 return template.read() % mapping_dict
172
173 def getTemplateFilename(self, template_name):
174 caller = inspect.stack()[1]
175 caller_frame = caller[0]
176 name = caller_frame.f_globals['__name__']
177 return pkg_resources.resource_filename(name,
178 'template/%s' % template_name)
179
180 def generatePassword(self, len_=32):
181 """
182 The purpose of this method is to generate a password which doesn't change
183 from one execution to the next, so the generated password doesn't change
184 on each slapgrid-cp execution.
185
186 Currently, it returns a hardcoded password because no decision has been
187 taken on where a generated password should be kept (so it is generated
188 once only).
189 """
190 # TODO: implement a real password generator which remember the last
191 # call.
192 return "insecure"
193
194 def isTrueValue(self, value):
195 return str(value).lower() in GenericBaseRecipe.TRUE_VALUES
196
197 def optionIsTrue(self, optionname, default=None):
198 if default is not None and optionname not in self.options:
199 return default
200 return self.isTrueValue(self.options[optionname])
201
202 def unparseUrl(self, scheme, host, path='', params='', query='',
203 fragment='', port=None, auth=None):
204 """Join a url with auth, host, and port.
205
206 * auth can be either a login string or a tuple (login, password).
207 * if the host is an ipv6 address, brackets will be added to surround it.
208
209 """
210 netloc = ''
211 if auth is not None:
212 auth = tuple(auth)
213 netloc = urllib.quote(str(auth[0])) # Login
214 if len(auth) > 1:
215 netloc += ':%s' % urllib.quote(auth[1]) # Password
216 netloc += '@'
217
218 # host is an ipv6 address whithout brackets
219 if ':' in host and not re.match(r'^\[.*\]$', host):
220 netloc += '[%s]' % host
221 else:
222 netloc += str(host)
223
224 if port is not None:
225 netloc += ':%s' % port
226
227 url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
228
229 return url
230
231 def setLocationOption(self):
232 if not self.options.get('location'):
233 self.options['location'] = os.path.join(
234 self.buildout['buildout']['parts-directory'], self.name)
235
236 def download(self, destination=None):
237 """ A simple wrapper around h.r.download, downloading to self.location"""
238 self.setLocationOption()
239
240 import hexagonit.recipe.download
241 if not destination:
242 destination = self.location
243 if os.path.exists(destination):
244 # leftovers from a previous failed attempt, removing it.
245 log.warning('Removing already existing directory %s' % destination)
246 shutil.rmtree(destination)
247 os.mkdir(destination)
248
249 try:
250 options = self.options.copy()
251 options['destination'] = destination
252 hexagonit.recipe.download.Recipe(
253 self.buildout, self.name, options).install()
254 except:
255 shutil.rmtree(destination)
256 raise