promotion 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.yardopts +1 -0
- data/CHANGELOG +0 -0
- data/README +256 -0
- data/VERSION +1 -0
- data/bin/devolve +59 -0
- data/bin/evolve +60 -0
- data/bin/promote +67 -0
- data/deploy.xml +35 -0
- data/ext/promotion/extconf.rb +19 -0
- data/lib/promotion/application.rb +49 -0
- data/lib/promotion/config.rb +55 -0
- data/lib/promotion/enforcer.rb +322 -0
- data/lib/promotion/erb/crontab.erb +19 -0
- data/lib/promotion/erb/profile.erb +17 -0
- data/lib/promotion/erb/rc.conf.local.erb +20 -0
- data/lib/promotion/erb/sudoers.erb +22 -0
- data/lib/promotion/evolver.rb +126 -0
- data/lib/promotion/generator.rb +207 -0
- data/lib/promotion.rb +8 -0
- data/promotion.xsd +323 -0
- metadata +97 -0
@@ -0,0 +1,322 @@
|
|
1
|
+
require 'etc'
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Promotion # :nodoc:
|
6
|
+
|
7
|
+
# The Enforcer handles the first half of the promotion process: moving
|
8
|
+
# the files into place, setting permissions, ensuring that users and groups
|
9
|
+
# are set up correctly, etc.
|
10
|
+
class Enforcer
|
11
|
+
|
12
|
+
# returns the XML specification for the selected application
|
13
|
+
attr_reader :spec
|
14
|
+
|
15
|
+
# root
|
16
|
+
DEFAULT_FOLDER_OWNER = "root"
|
17
|
+
# wheel
|
18
|
+
DEFAULT_FOLDER_GROUP = "wheel"
|
19
|
+
# 0750
|
20
|
+
DEFAULT_FOLDER_MODE = "0750"
|
21
|
+
# false
|
22
|
+
DEFAULT_FOLDER_CLEAR = "false"
|
23
|
+
# root
|
24
|
+
DEFAULT_FILE_OWNER = "root"
|
25
|
+
# wheel
|
26
|
+
DEFAULT_FILE_GROUP = "wheel"
|
27
|
+
# 0640
|
28
|
+
DEFAULT_FILE_MODE = "0640"
|
29
|
+
# '.' the current folder
|
30
|
+
DEFAULT_FILE_SOURCE = "."
|
31
|
+
|
32
|
+
# Creates a new Enforcer for the selected application.
|
33
|
+
def initialize(appname)
|
34
|
+
appFolder = File.expand_path(appname, Folders::Staging)
|
35
|
+
Dir.chdir(appFolder)
|
36
|
+
specfile = File.expand_path(Files::Spec, appFolder)
|
37
|
+
unless File.exist?(specfile)
|
38
|
+
puts("\nSpecification file #{specfile} does not exist.\n" )
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
doc = REXML::Document.new(File.new(specfile))
|
42
|
+
@spec = doc.root
|
43
|
+
$log.info("\n#{'_'*40}\nEnforcer created for #{appname}")
|
44
|
+
end
|
45
|
+
|
46
|
+
# Enforces the conditions needed for the selected app to function properly,
|
47
|
+
# as specified in the deployment descriptor:
|
48
|
+
# - ensure that certain groups exist
|
49
|
+
# - ensure that certain users exist (belonging to certain groups)
|
50
|
+
# - ensure that certain folders exist, if not create them; set permissions correctly;
|
51
|
+
# if desired, make sure they are empty
|
52
|
+
# - move specified files into place, set ownerships and permissions;
|
53
|
+
# create empty, or make empty, or preserve contents as desired.
|
54
|
+
# - ensure symbolic link exists, create if missing
|
55
|
+
# - create a zip archive of certain files if desired.
|
56
|
+
def start()
|
57
|
+
@spec.elements.each("/Specification/Groups/Group") { |group|
|
58
|
+
gid = group.attributes["Gid"].to_i
|
59
|
+
name = group.attributes["Name"]
|
60
|
+
create_group(gid, name) unless group_exist?(gid, name)
|
61
|
+
}
|
62
|
+
@spec.elements.each("/Specification/Users/User") { |user|
|
63
|
+
uid = user.attributes["Uid"].to_i
|
64
|
+
name = user.attributes["Name"]
|
65
|
+
gid = user.attributes["Gid"].to_i
|
66
|
+
uclass = user.attributes["Class"] || "default"
|
67
|
+
gecos = user.attributes["Gecos"] || ""
|
68
|
+
home = user.attributes["Home"] || "/home/#{name}"
|
69
|
+
shell = user.attributes["Shell"] || "/bin/ksh"
|
70
|
+
groups = user.attributes["Groups"] || ""
|
71
|
+
create_user(uid, name, gid, uclass, gecos, home, shell, groups) unless user_exist?(uid, name)
|
72
|
+
}
|
73
|
+
@spec.elements.each("/Specification/Folders/Folder[@Clear='true']") { |folder|
|
74
|
+
path = folder.text().strip()
|
75
|
+
clear_folder(path)
|
76
|
+
}
|
77
|
+
@spec.elements.each("/Specification/Folders/Folder") { |folder|
|
78
|
+
path = folder.text().strip()
|
79
|
+
owner = folder.attributes["Owner"] || folder.parent.attributes["Owner"] || DEFAULT_FOLDER_OWNER
|
80
|
+
group = folder.attributes["Group"] || folder.parent.attributes["Group"] || DEFAULT_FOLDER_GROUP
|
81
|
+
mode = folder.attributes["Mode"] || folder.parent.attributes["Mode"] || DEFAULT_FOLDER_MODE
|
82
|
+
clear = folder.attributes["Clear"] || folder.parent.attributes["Clear"] || DEFAULT_FOLDER_MODE
|
83
|
+
mode = mode.oct()
|
84
|
+
clear = (clear == "true")
|
85
|
+
ensure_folder(path, owner, group, mode, clear)
|
86
|
+
}
|
87
|
+
@spec.elements.each("/Specification/Files/File") { |file|
|
88
|
+
path = file.text().strip()
|
89
|
+
owner = file.attributes["Owner"] || file.parent.attributes["Owner"] || DEFAULT_FILE_OWNER
|
90
|
+
group = file.attributes["Group"] || file.parent.attributes["Group"] || DEFAULT_FILE_GROUP
|
91
|
+
mode = file.attributes["Mode"] || file.parent.attributes["Mode"] || DEFAULT_FILE_MODE
|
92
|
+
mode = mode.oct()
|
93
|
+
source = file.attributes["Source"] || file.parent.attributes["Source"] || DEFAULT_FILE_SOURCE
|
94
|
+
empty = file.attributes["Empty"] == "true"
|
95
|
+
overwrite = !(file.attributes["Overwrite"] == "false")
|
96
|
+
backup = file.attributes["Backup"] == "true"
|
97
|
+
ensure_file(path, owner, group, mode, source, empty, overwrite, backup)
|
98
|
+
}
|
99
|
+
@spec.elements.each("/Specification/Files/Link") { |link|
|
100
|
+
path = link.text().strip()
|
101
|
+
owner = link.attributes["Owner"] || link.parent.attributes["Owner"] || DEFAULT_FILE_OWNER
|
102
|
+
group = link.attributes["Group"] || link.parent.attributes["Group"] || DEFAULT_FILE_GROUP
|
103
|
+
mode = link.attributes["Mode"] || link.parent.attributes["Mode"] || DEFAULT_FILE_MODE
|
104
|
+
mode = mode.oct()
|
105
|
+
target = link.attributes["Target"] || link.parent.attributes["Source"] || DEFAULT_FILE_SOURCE
|
106
|
+
ensure_link(path, owner, group, mode, target)
|
107
|
+
}
|
108
|
+
@spec.elements.each("/Specification/Allfiles") { |file|
|
109
|
+
path = file.text().strip()
|
110
|
+
owner = file.attributes["Owner"] || file.parent.attributes["Owner"] || DEFAULT_FILE_OWNER
|
111
|
+
group = file.attributes["Group"] || file.parent.attributes["Group"] || DEFAULT_FILE_GROUP
|
112
|
+
mode = file.attributes["Mode"] || file.parent.attributes["Mode"] || DEFAULT_FILE_MODE
|
113
|
+
mode = mode.oct()
|
114
|
+
source = file.attributes["Source"] || file.parent.attributes["Source"] || DEFAULT_FILE_SOURCE
|
115
|
+
ensure_allfiles(path, owner, group, mode, source)
|
116
|
+
}
|
117
|
+
@spec.elements.each("/Specification/Zipfile") { |zipfile|
|
118
|
+
build_zip_file(zipfile)
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
# Detects if a user account exists with the given +uid+ and +name+
|
123
|
+
def user_exist?(uid, name)
|
124
|
+
begin
|
125
|
+
user1 = Etc.getpwuid(uid)
|
126
|
+
user2 = Etc.getpwnam(name)
|
127
|
+
raise unless user1 == user2
|
128
|
+
$log.info("User #{name}(#{uid}) already exists.")
|
129
|
+
rescue
|
130
|
+
return(false)
|
131
|
+
end
|
132
|
+
return(true)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Creates a user account for the operating system
|
136
|
+
def create_user(uid, name, gid, uclass, gecos, home, shell, groups)
|
137
|
+
begin
|
138
|
+
FileUtils.mkdir_p(home) unless File.directory?(home) # ensure no warning about missing folder
|
139
|
+
command = Files::Useradd + " -u #{uid} -g #{gid} -L #{uclass} "
|
140
|
+
command += "-c '#{gecos}' -d #{home} -s #{shell} -p '*************' "
|
141
|
+
command += "-G #{groups} " if groups.length > 0
|
142
|
+
command += " #{name} "
|
143
|
+
raise unless system(command)
|
144
|
+
$log.info("User #{name}(#{uid})created.")
|
145
|
+
rescue => e
|
146
|
+
$log.error("Unable to create user #{name}(#{uid})\n#{e.message}")
|
147
|
+
exit 1
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Detects if a group exists with the given +gid+ and +name+
|
152
|
+
def group_exist?(gid, name)
|
153
|
+
begin
|
154
|
+
group1 = Etc.getgrgid(gid)
|
155
|
+
group2 = Etc.getgrnam(name)
|
156
|
+
raise unless group1 == group2
|
157
|
+
$log.info("Group #{name}(#{gid}) already exists.")
|
158
|
+
rescue
|
159
|
+
return(false)
|
160
|
+
end
|
161
|
+
return(true)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Create a group in the operating system
|
165
|
+
def create_group(gid, name)
|
166
|
+
begin
|
167
|
+
command = Files::Groupadd + " -v -g #{gid} #{name}"
|
168
|
+
raise unless system(command)
|
169
|
+
$log.info("Group #{name}(#{gid}) created.")
|
170
|
+
rescue => e
|
171
|
+
$log.error("Unable to create group #{name}(#{gid})\n#{e.message}")
|
172
|
+
exit 1
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Removes a folder unconditionally
|
177
|
+
def clear_folder(path)
|
178
|
+
begin
|
179
|
+
FileUtils.rm_rf(path)
|
180
|
+
$log.info("Removed folder #{path} in preparation for a fresh installation.")
|
181
|
+
rescue
|
182
|
+
$log.error("Unable to remove folder #{path} prior to installation.")
|
183
|
+
exit 1
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Creates a folder (and any intermediate parent folders), sets the ownership
|
188
|
+
# and permissions
|
189
|
+
def ensure_folder(path, owner, group, mode, clear)
|
190
|
+
begin
|
191
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
192
|
+
FileUtils.chown(owner, group, path)
|
193
|
+
FileUtils.chmod(mode, path)
|
194
|
+
$log.info("Ensured folder #{path} is owned by #{owner}:#{group} with mode #{sprintf('%04o',mode)}")
|
195
|
+
rescue => e
|
196
|
+
$log.error("Unable to ensure folder #{path} is owned by #{owner}:#{group} with mode #{sprintf('%04o',mode)}\n#{e.message}")
|
197
|
+
exit 1
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Ensures that all specified files are moved into place with the correct ownership
|
202
|
+
# and permissions
|
203
|
+
def ensure_allfiles(path, owner, group, mode, source)
|
204
|
+
unless File.directory?(source)
|
205
|
+
$log.error("Missing source folder #{source} to install contents to #{path}")
|
206
|
+
exit 1
|
207
|
+
end
|
208
|
+
# modify the mode for folders so they are executable
|
209
|
+
perms = sprintf('%9b', mode)
|
210
|
+
perms[2] = "1"
|
211
|
+
perms[5] = "1"
|
212
|
+
perms[8] = "1"
|
213
|
+
folderMode = perms.to_i(2)
|
214
|
+
begin
|
215
|
+
Dir.chdir(source)
|
216
|
+
Dir["**/*"].each { |file|
|
217
|
+
targetPath = File.expand_path(file, path) # file could be app/media/shared/mypic.png
|
218
|
+
if File.directory?(file)
|
219
|
+
ensure_folder(targetPath, owner, group, folderMode, false)
|
220
|
+
else
|
221
|
+
ensure_file(targetPath, owner, group, mode, File.dirname(file), false, true)
|
222
|
+
end
|
223
|
+
}
|
224
|
+
rescue => e
|
225
|
+
$log.error("Unable to install allfiles from #{source} to #{path}\n#{e.message}")
|
226
|
+
exit 1
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Ensures that a file exists at the given path, with the
|
231
|
+
# ownership and permissions specified.
|
232
|
+
# The source file is derived from the source folder and path basename
|
233
|
+
# If the +empty+ parameter is +true+, create an empty file
|
234
|
+
# If <code>Overwrite="false"</code> then set ownerships and permissions but leave
|
235
|
+
# the contents alone (ie. create but no update)
|
236
|
+
# +path+ = full path to file to be created
|
237
|
+
# +source+ = folder within the project to copy file from
|
238
|
+
# If +backup+, preserve a copy of the original file if it has never been backed
|
239
|
+
# up yet (good for preserving original system config files)
|
240
|
+
def ensure_file(path, owner, group, mode, source, empty, overwrite=true, backup=false)
|
241
|
+
unless empty
|
242
|
+
sourceFile = File.expand_path(File.basename(path), source)
|
243
|
+
unless File.exist?(sourceFile)
|
244
|
+
$log.error("Missing source file #{sourceFile} to install to #{path}")
|
245
|
+
exit 1
|
246
|
+
end
|
247
|
+
end
|
248
|
+
begin
|
249
|
+
targetFolder = File.dirname(path)
|
250
|
+
unless File.directory?(targetFolder)
|
251
|
+
$log.warn("Missing folder #{targetFolder} is being created but may have wrong ownership and permissions.")
|
252
|
+
FileUtils.mkdir_p(targetFolder)
|
253
|
+
end
|
254
|
+
if empty
|
255
|
+
FileUtils.touch(path)
|
256
|
+
elsif !overwrite and File.exists?(path)
|
257
|
+
# preserve the contents
|
258
|
+
else
|
259
|
+
FileUtils.cp(path, path+"-original") if backup and not File.exist?(path+"-original")
|
260
|
+
FileUtils.cp(sourceFile, path)
|
261
|
+
end
|
262
|
+
FileUtils.chown(owner, group, path)
|
263
|
+
FileUtils.chmod(mode, path)
|
264
|
+
$log.info("Installed file #{path}, owned by #{owner}:#{group} with mode #{sprintf('%04o',mode)}")
|
265
|
+
rescue => e
|
266
|
+
$log.error("Unable to install file #{path}, owned by #{owner}:#{group} with mode #{sprintf('%04o',mode)}\n#{e.message}")
|
267
|
+
exit 1
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Ensures that a link exists at the given path, with the ownership and permissions specified.
|
272
|
+
def ensure_link(path, owner, group, mode, target)
|
273
|
+
begin
|
274
|
+
targetFolder = File.dirname(path)
|
275
|
+
unless File.directory?(targetFolder)
|
276
|
+
$log.warn("Missing folder #{targetFolder} is being created but may have wrong ownership and permissions.")
|
277
|
+
FileUtils.mkdir_p(targetFolder)
|
278
|
+
end
|
279
|
+
FileUtils.ln_sf(target, path)
|
280
|
+
FileUtils.chown(owner, group, path)
|
281
|
+
FileUtils.chmod(mode, path)
|
282
|
+
$log.info("Installed link #{path} --> #{target}, owned by #{owner}:#{group} with mode #{sprintf('%04o',mode)}")
|
283
|
+
rescue => e
|
284
|
+
$log.error("Unable to install link #{path} --> #{target}, owned by #{owner}:#{group} with mode #{sprintf('%04o',mode)}\n#{e.message}")
|
285
|
+
exit 1
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Creates a zip archive from the specified files
|
290
|
+
def build_zip_file(zipfile)
|
291
|
+
begin
|
292
|
+
path = zipfile.elements["Zip"].text().strip()
|
293
|
+
owner = zipfile.elements["Zip"].attributes["Owner"] || "root"
|
294
|
+
group = zipfile.elements["Zip"].attributes["Group"] || "wheel"
|
295
|
+
mode = zipfile.elements["Zip"].attributes["Mode"] || "0644"
|
296
|
+
mode = mode.oct()
|
297
|
+
filelist = []
|
298
|
+
zipfile.elements.each("Source") { |src|
|
299
|
+
sourceFile = src.text().strip()
|
300
|
+
unless File.exist?(sourceFile)
|
301
|
+
$log.error("Missing source file #{sourceFile} to add to zip archive #{path}")
|
302
|
+
exit 1
|
303
|
+
end
|
304
|
+
filelist << sourceFile
|
305
|
+
}
|
306
|
+
targetFolder = File.dirname(path)
|
307
|
+
unless File.directory?(targetFolder)
|
308
|
+
$log.warn("Missing folder #{targetFolder} is being created but may have wrong ownership and permissions.")
|
309
|
+
FileUtils.mkdir_p(targetFolder)
|
310
|
+
end
|
311
|
+
system("#{Files::Zip} #{path} #{filelist.join(' ')} ")
|
312
|
+
FileUtils.chown(owner, group, path)
|
313
|
+
FileUtils.chmod(mode, path)
|
314
|
+
$log.info("Created zip archive #{path}, owned by #{owner}:#{group} with mode #{sprintf('%04o',mode)}")
|
315
|
+
rescue => e
|
316
|
+
$log.error("Unable to create zip archive #{path}, owned by #{owner}:#{group} with mode #{sprintf('%04o',mode)}\n#{e.message}\n#{e.backtrace}")
|
317
|
+
exit 1
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
end
|
322
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#
|
2
|
+
# /var/cron/tabs/<%= user %> - <%= user %>'s crontab
|
3
|
+
#
|
4
|
+
#minute hour mday month wday command
|
5
|
+
#
|
6
|
+
<% crontablist.each do |sched| %>
|
7
|
+
<% if sched.attributes["Comment"] %>
|
8
|
+
#______________________________
|
9
|
+
# <%= sched.attributes["Comment"] %>
|
10
|
+
<% end %>
|
11
|
+
<% minute = sched.attributes["Minute"] || "*" %>
|
12
|
+
<% hour = sched.attributes["Hour"] || "*" %>
|
13
|
+
<% mday = sched.attributes["DayOfMonth"] || "*" %>
|
14
|
+
<% month = sched.attributes["Month"] || "*" %>
|
15
|
+
<% wday = sched.attributes["DayOfWeek"] || "*" %>
|
16
|
+
<% cmd = (sched.elements["Command"].cdatas[0]).value().strip() %>
|
17
|
+
<%= minute + "\t" + hour + "\t" + mday + "\t" + month + "\t" + wday + "\t" + cmd %>
|
18
|
+
|
19
|
+
<% end %>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# This section of the file should not be edited
|
2
|
+
# It was generated by the promotion application and will be overwritten
|
3
|
+
# when the next promotion occurs.
|
4
|
+
# The previous section will be preserved
|
5
|
+
<% @specs.each do |spec|
|
6
|
+
spec.elements.each("/Specification/Environment") do |env|
|
7
|
+
env.elements.each("Variable") do |var|
|
8
|
+
t = var.cdatas.length > 0 ? var.cdatas[0].to_s() : var.text() %>
|
9
|
+
export <%= var.attributes["Name"] %>="<%= t.strip() %>"<% end %>
|
10
|
+
<% env.elements.each("Alias") do |ali|
|
11
|
+
t = ali.cdatas.length > 0 ? ali.cdatas[0].to_s() : ali.text()
|
12
|
+
%><% if ali.attributes["Comment"] %><%= "\n# "+ali.attributes["Comment"]
|
13
|
+
%><% end %>
|
14
|
+
alias <%= ali.attributes["Name"] %>='<%= t %>' <%
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end %>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# This section of the file is generated by the promotion application
|
2
|
+
# DO NOT EDIT THIS SECTION - your changes will be lost
|
3
|
+
# You may edit the section before the promotion marker
|
4
|
+
#______________________________
|
5
|
+
# generated application flags
|
6
|
+
<% daemonSpecs = @specs.reject {|s| s.elements["/Specification/Daemon"].nil? } %>
|
7
|
+
<% daemonSpecs.sort!() do |a, b| %>
|
8
|
+
<% ap = a.elements["/Specification/Daemon"].attributes["Priority"].to_i || 10 %>
|
9
|
+
<% bp = b.elements["/Specification/Daemon"].attributes["Priority"].to_i || 10 %>
|
10
|
+
<% ap <=> bp %>
|
11
|
+
<% end %>
|
12
|
+
<% pkgScripts = [] %>
|
13
|
+
<% daemonSpecs.each do |spec| %>
|
14
|
+
<%= spec.attributes["Name"] %>_flags="<%= spec.elements["/Specification/Daemon"].attributes["Flags"] %>"
|
15
|
+
<% pkgScripts << spec.attributes["Name"] %>
|
16
|
+
<% end %>
|
17
|
+
|
18
|
+
# rc.d(8) packages scripts
|
19
|
+
# started in the specified order and stopped in reverse order
|
20
|
+
pkg_scripts="<%= pkgScripts.join(" ") %>"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This section of the file should not be edited, even with visudo
|
2
|
+
# It was generated by the promotion application and will be overwritten
|
3
|
+
# when the next promotion occurs.
|
4
|
+
# The previous section will be preserved
|
5
|
+
|
6
|
+
Defaults timestamp_timeout=55
|
7
|
+
|
8
|
+
root ALL = (ALL) ALL
|
9
|
+
# people in group wheel may run all commands
|
10
|
+
%wheel ALL = (ALL) ALL
|
11
|
+
|
12
|
+
# Generated user privilege specifications
|
13
|
+
<% @specs.each do |spec| %>
|
14
|
+
<% spec.elements.each("/Specification/Sudoers/UserPrivilege") do |priv| %>
|
15
|
+
<%= sprintf('%-16s', priv.attributes["User"]) %>
|
16
|
+
<%= " ALL = " %>
|
17
|
+
<% if priv.attributes["Runas"] %>(<%= priv.attributes["Runas"] %>) <% end %>
|
18
|
+
<%= (priv.attributes["Password"] || "").downcase() == "true" ? " " : "NOPASSWD: " %>
|
19
|
+
<%= priv.text().strip() %>
|
20
|
+
|
21
|
+
<% end %>
|
22
|
+
<% end %>
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Promotion # :nodoc:
|
2
|
+
# The Evolver class evolves the database by executing migration scripts from the
|
3
|
+
# evolve folder of the project being promoted
|
4
|
+
#
|
5
|
+
# This class may be invoked via promote or via evolve or devolve commands
|
6
|
+
class Evolver
|
7
|
+
|
8
|
+
# Creates a new Evolver
|
9
|
+
def initialize(appname, evolve=true, targetVersion=nil)
|
10
|
+
@appname = appname
|
11
|
+
@evolve = (evolve == true)
|
12
|
+
@target = targetVersion.to_i()
|
13
|
+
@currentVersion = nil
|
14
|
+
@spec = get_spec()
|
15
|
+
db = @spec.elements["Database"]
|
16
|
+
@dbms = db.text()
|
17
|
+
@database = db.attributes["database"] || ""
|
18
|
+
end
|
19
|
+
|
20
|
+
# The deployment descriptor for an application should contain a single Database element
|
21
|
+
# in order to make use of the +evolve+ command. SQLite3 also needs a +database+
|
22
|
+
# attribute to specify the file to operate on.
|
23
|
+
# <Database>/usr/bin/mysql</Database>
|
24
|
+
# <Database database="/var/myapp/myapp.db">/usr/bin/sqlite3</Database>
|
25
|
+
def get_spec()
|
26
|
+
appFolder = File.expand_path(@appname, Folders::Staging)
|
27
|
+
Dir.chdir(appFolder)
|
28
|
+
specfile = File.expand_path(Files::Spec, appFolder)
|
29
|
+
unless File.exist?(specfile)
|
30
|
+
puts("\nSpecification file #{specfile} does not exist.\n" )
|
31
|
+
exit 1
|
32
|
+
end
|
33
|
+
doc = REXML::Document.new(File.new(specfile))
|
34
|
+
doc.root
|
35
|
+
end
|
36
|
+
|
37
|
+
# Gets the current version from the version file and calls evolve or devolve as required
|
38
|
+
def start()
|
39
|
+
versionFilename = File.expand_path("@version.#{@appname}", Folders::Staging)
|
40
|
+
if !File.exist?(versionFilename)
|
41
|
+
puts("We expect a version file at #{versionFilename} containing a single line")
|
42
|
+
puts("with the current version of the database (eg. 1001)")
|
43
|
+
exit 1
|
44
|
+
end
|
45
|
+
versionFile = File.new(versionFilename, 'r')
|
46
|
+
@currentVersion = versionFile.gets.chomp().to_i()
|
47
|
+
versionFile.close()
|
48
|
+
completed = @currentVersion
|
49
|
+
if @evolve
|
50
|
+
completed = evolve()
|
51
|
+
else
|
52
|
+
completed = devolve()
|
53
|
+
end
|
54
|
+
versionFile = File.new(versionFilename, 'w')
|
55
|
+
versionFile.puts(completed)
|
56
|
+
versionFile.close()
|
57
|
+
puts(" to version #{completed} ")
|
58
|
+
end
|
59
|
+
|
60
|
+
# Starts the database evolution:
|
61
|
+
# - find all of the relevant schema migration files in the +evolve+ folder
|
62
|
+
# - execute in sequence all migrations after the current version, updating the
|
63
|
+
# version file with the latest successful version number
|
64
|
+
def evolve()
|
65
|
+
$log.info("\n#{'_'*40}\nEvolving the database #{@database}\n")
|
66
|
+
evolveFolder = File.expand_path("#{@appname}/evolve", Folders::Staging)
|
67
|
+
Dir.chdir(evolveFolder)
|
68
|
+
migrations = Dir["*.sql"].collect { |f| f.sub(".sql","").to_i() }.sort()
|
69
|
+
migrations.reject! { |v| v <= @currentVersion }
|
70
|
+
migrations.reject! { |v| v > @target } unless @target == 0
|
71
|
+
@target = migrations.last.to_i
|
72
|
+
if @target > @currentVersion
|
73
|
+
print("Evolving the database")
|
74
|
+
else
|
75
|
+
puts("Already at version #{@currentVersion}.")
|
76
|
+
exit 0
|
77
|
+
end
|
78
|
+
completed = @currentVersion
|
79
|
+
migrations.each { |v|
|
80
|
+
success = system("#{@dbms} #{@database} < #{v}.sql")
|
81
|
+
if success
|
82
|
+
$log.info("Evolved the database to version #{v}")
|
83
|
+
completed = v
|
84
|
+
else
|
85
|
+
$log.error("Failed to evolve the database to version #{v}.")
|
86
|
+
break
|
87
|
+
end
|
88
|
+
}
|
89
|
+
return(completed)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns the database to an earlier schema version:
|
93
|
+
# - find all of the relevant schema migration files in the +devolve+ folder
|
94
|
+
# - execute in sequence all migrations from the current version down to
|
95
|
+
# the target version
|
96
|
+
# - update the version file with the new version number
|
97
|
+
def devolve()
|
98
|
+
$log.info("\n#{'_'*40}\nDevolving the database #{@database}\n")
|
99
|
+
devolveFolder = File.expand_path("#{@appname}/devolve", Folders::Staging)
|
100
|
+
Dir.chdir(devolveFolder)
|
101
|
+
migrations = Dir["*.sql"].collect { |f| f.sub(".sql","").to_i() }.sort()
|
102
|
+
migrations.reject! { |v| v > @currentVersion }
|
103
|
+
migrations.reject! { |v| v <= @target } # after devolving we are at the previous version
|
104
|
+
migrations.reverse!
|
105
|
+
if @target < @currentVersion
|
106
|
+
print("Devolving the database")
|
107
|
+
else
|
108
|
+
puts("Already at version #{@currentVersion}.")
|
109
|
+
exit 0
|
110
|
+
end
|
111
|
+
completed = @currentVersion
|
112
|
+
migrations.each { |v|
|
113
|
+
success = system("#{@dbms} #{@database} < #{v}.sql")
|
114
|
+
if success
|
115
|
+
$log.info("Devolved the database from version #{v}")
|
116
|
+
completed = v
|
117
|
+
else
|
118
|
+
$log.error("Failed to devolve the database from version #{v}.")
|
119
|
+
break
|
120
|
+
end
|
121
|
+
}
|
122
|
+
return(completed-1) # after devolving we are at the previous version
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|