promotion 1.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--title "Promotion" -m rdoc --main README lib/**/*.rb - README
|
data/CHANGELOG
ADDED
File without changes
|
data/README
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
= Promotion
|
2
|
+
Promotion makes it possible to repeatedly deploy an application in a fast and
|
3
|
+
reliable way.
|
4
|
+
|
5
|
+
== Staging
|
6
|
+
A key concept is the *staging* area. It is a folder (+/var/staging+ by default)
|
7
|
+
where the files for each application are loaded into a named sub-folder (eg. +/var/staging/myapp+).
|
8
|
+
For example, a staging area may look like this:
|
9
|
+
|
10
|
+
/var
|
11
|
+
|__ staging
|
12
|
+
|__ webapp1 -- a web application
|
13
|
+
|__ monitor -- sysadmin tool to monitor availability
|
14
|
+
|__ analytics -- analytics app for admins
|
15
|
+
|__ snort -- a custom config for snort
|
16
|
+
|
17
|
+
In this example, note that we also have an area for an installed application like snort.
|
18
|
+
Although the binaries are installed through normal package installation,
|
19
|
+
you may have a lot of time and work invested in the
|
20
|
+
configuration. These files can of course be backed up and restored manually, but the purpose of
|
21
|
+
Promotion is to make deployment fast and reliable. One way to achieve that is to store your
|
22
|
+
valuable configuration files in source control, so you can reliably checkout the latest version
|
23
|
+
for deployment each and every time. So the +snort+ staging area need only hold a few configuration
|
24
|
+
files.
|
25
|
+
|
26
|
+
Aside: why not just use version control?::
|
27
|
+
The important files are spread all over the file system, so the only way to restore them all
|
28
|
+
in a single command is to make the root directory a subversion working folder, add selected
|
29
|
+
files to version control and then have <code>.svn</code> folders spread all over the place.
|
30
|
+
Moreover, you'd need to run svn as root.
|
31
|
+
|
32
|
+
== Deployment Descriptor
|
33
|
+
Each application has an XML <b>deployment descriptor</b> (eg. +/var/staging/myapp/deploy.xml+)
|
34
|
+
that describes the conditions necessary for the successful operation of the application.
|
35
|
+
|
36
|
+
The deployment descriptor describes how to move the files from staging to their correct
|
37
|
+
locations in the file system. This simple requirement unrolls into a chain of others:
|
38
|
+
- we need the folder to be there to put the file in
|
39
|
+
- the permissions need to be correct on the file and folder
|
40
|
+
- the ownerships must be set
|
41
|
+
- that means we need the necessary user accounts created
|
42
|
+
- which means we first need the right groups set up.
|
43
|
+
|
44
|
+
In addition to installing the application's files, there are system-wide configuration
|
45
|
+
files that need to be adjusted to enable an application to startup or run properly:
|
46
|
+
- /etc/profile
|
47
|
+
- /etc/rc.conf.local
|
48
|
+
- /etc/sudoers
|
49
|
+
- /var/cron/tabs/*
|
50
|
+
|
51
|
+
Promotion generates these files based on the set of all of the deployment descriptors it finds
|
52
|
+
in the staging area.
|
53
|
+
|
54
|
+
An important aspect is that a deployment is *idempotent* - you can do it as many
|
55
|
+
times as you like and the result will be the same. This means its safe to make a
|
56
|
+
small change to an application and re-deploy it in a few seconds, ready for testing.
|
57
|
+
|
58
|
+
Promotion is useful in a testing environment when changes and redeployments are frequent,
|
59
|
+
but it shines in a production environment by reducing maintenance downtime and risk.
|
60
|
+
|
61
|
+
Promotion was originally designed for use on an OpenBSD system, but is configurable enough to work
|
62
|
+
on any *nix system. Just modify the <code>promotion/config.rb</code> configuration file
|
63
|
+
to suit your system's paths.
|
64
|
+
|
65
|
+
== Installation
|
66
|
+
*WARNING*::
|
67
|
+
Promotion will makes changes to your operating system, including important system-wide files.
|
68
|
+
|
69
|
+
DO NOT USE ON A PRODUCTION SYSTEM without rigorous testing in a virtual machine first.
|
70
|
+
|
71
|
+
Create the staging area (and set permissions appropriately):
|
72
|
+
|
73
|
+
$ sudo mkdir /var/staging
|
74
|
+
|
75
|
+
Check out the promotion app into staging, and bootstrap promotion
|
76
|
+
$ sudo wget http://rubyforge.org/frs/download.php/76463/promotion-x.y.z.zip
|
77
|
+
$ sudo unzip promotion-x.y.z.zip -d promotion # creates the promotion sub-directory
|
78
|
+
$ cd promotion
|
79
|
+
$ sudo ruby bin/bootstrap
|
80
|
+
Promoting promotion...OK.
|
81
|
+
$ cd /var/staging
|
82
|
+
Now you are ready to use Promotion to deploy other applications.
|
83
|
+
|
84
|
+
== Usage
|
85
|
+
For each application you want to deploy, create its staging folder:
|
86
|
+
$ cd /var/staging
|
87
|
+
$ sudo svn checkout https://hosted/path/to/project/dist myapp
|
88
|
+
The project (or perhaps a +dist+ folder designed for deployment) is checked out into
|
89
|
+
a folder with the given name.
|
90
|
+
|
91
|
+
Now you can promote the application:
|
92
|
+
$ sudo promote myapp
|
93
|
+
Promoting myapp...OK.
|
94
|
+
|
95
|
+
If a database is involved, you can also migrate the database schema:
|
96
|
+
$ sudo evolve myapp
|
97
|
+
Evolving the database to version 1023
|
98
|
+
which will execute any new schema migration scripts. If you execute it again,
|
99
|
+
you will see:
|
100
|
+
$ sudo evolve myapp
|
101
|
+
Already at version 1023.
|
102
|
+
|
103
|
+
If you want to revert to an earlier version of the database schema:
|
104
|
+
$ sudo devolve myapp 1017
|
105
|
+
Devolving the database to version 1017
|
106
|
+
which will execute the schema migration scripts in the +devolve+ folder,
|
107
|
+
in reverse order from 1023 down to 1017.
|
108
|
+
|
109
|
+
== Deployment descriptor syntax
|
110
|
+
The deployment descriptor is typically named <code>deploy.xml</code> and placed at the top
|
111
|
+
of the application's staging folder (eg. <code>/var/staging/myapp</code>).
|
112
|
+
|
113
|
+
It's structure is described by the XML schema which can be found at
|
114
|
+
at +/var/staging/promotion/promotion.xsd+ or at
|
115
|
+
http://finalstep.com.au/schema/promotion.v100.xsd
|
116
|
+
|
117
|
+
=== +Specification+
|
118
|
+
The top level element is a Specification
|
119
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
120
|
+
<Specification Name="myapp" Title="My Test App"
|
121
|
+
xmlns="http://finalstep.com.au/promotion/v100"
|
122
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
123
|
+
xsi:schemaLocation="http://finalstep.com.au/schema/promotion.v100.xsd ../promotion/promotion.xsd">
|
124
|
+
which has a +Name+ matching the staging folder it resides in (+myapp+), and a fuller +Title+.
|
125
|
+
|
126
|
+
=== +Description+
|
127
|
+
A description element is the first child:
|
128
|
+
<Description>
|
129
|
+
The promotion manager ensures that the environment for an application
|
130
|
+
is set up correctly, including folder and file permissions and ownership,
|
131
|
+
startup scripts, sudoers privileges and environment variables.
|
132
|
+
</Description>
|
133
|
+
|
134
|
+
=== +Groups+
|
135
|
+
The sequence is then driven by dependencies: you cannot set ownerships until you have
|
136
|
+
users, and you cannot create a user without a group, so Groups come first. Each of these
|
137
|
+
elements is optional however.
|
138
|
+
<Groups>
|
139
|
+
<Group Gid="502" Name="_mysql" />
|
140
|
+
</Groups>
|
141
|
+
This creates a group with group ID <code>502</code> and the name <code>_mysql</code>.
|
142
|
+
|
143
|
+
=== +Users+
|
144
|
+
We can also create an admin and a user to run the MySQL daemon:
|
145
|
+
<Users>
|
146
|
+
<User Name="richard" Class="staff" Gecos="Richard Kernahan" Uid="1002" Gid="1002"
|
147
|
+
Home="/home/richard" Shell="/bin/ksh" Groups="wheel webadmin"/>
|
148
|
+
<User Name="_mysql" Class="daemon" Gecos="MySQL Account" Uid="502"Gid="502"
|
149
|
+
Home="/nonexistent" Shell="/sbin/nologin" />
|
150
|
+
</Users>
|
151
|
+
|
152
|
+
- +Name+, +Uid+, and +Gid+ are mandatory
|
153
|
+
- +Gecos+ defaults to the Name
|
154
|
+
- +Home+ for +richard+ defaults to +/home/richard+
|
155
|
+
- +Shell+ defaults to +/sbin/nologin+
|
156
|
+
- +Groups+ is an optional space-separated list of groups the user should be added to
|
157
|
+
|
158
|
+
=== +Folders+
|
159
|
+
Before we can move files into place, we need the Folders set up:
|
160
|
+
<Folders Mode="0750" Owner="root" Group="wheel">
|
161
|
+
<Folder Owner="_mysql" Group="_mysql">/var/mysql</Folder>
|
162
|
+
<Folder>/home/myapp</Folder>
|
163
|
+
<Folder>/var/myapp</Folder>
|
164
|
+
</Folders>
|
165
|
+
You can have several +Folders+ elements, if desired. The defaults for all +Folder+
|
166
|
+
child elements may be set as attributes of the parent +Folders+ element. In this example,
|
167
|
+
all the folders will have permissions <code>rwxr-x---</code>, and ownership of <code>root:wheel</code>.
|
168
|
+
Note that the folder <code>/var/mysql</code> has a different owner and group. Any Folder can
|
169
|
+
override the default Mode, Owner, and Group.
|
170
|
+
|
171
|
+
=== +Files+
|
172
|
+
Finally we can specify how to move the files into place:
|
173
|
+
<Files Owner="root" Group="wheel" Mode="0644" Source="conf">
|
174
|
+
<File>/etc/my.cnf</File>
|
175
|
+
<File>/etc/myapp.conf</File>
|
176
|
+
</Files>
|
177
|
+
<Files Owner="root" Group="wheel" Mode="0750" Source="sbin">
|
178
|
+
<File>/usr/local/sbin/up!</File>
|
179
|
+
<File>/usr/local/sbin/down!</File>
|
180
|
+
<File>/usr/local/sbin/push!</File>
|
181
|
+
</Files>
|
182
|
+
|
183
|
+
In this example, we have two sets of files. The first set is expected to be found under the +conf+
|
184
|
+
folder. Looking at the first file, we see the destination path +/etc/my.cnf+.
|
185
|
+
The source file should have the same filename +my.cnf+ and be located in the +conf+ folder.
|
186
|
+
This results in Promotion executing a command such as:
|
187
|
+
|
188
|
+
cp -f /var/staging/myapp/conf/my.cnf /etc/my.cnf
|
189
|
+
|
190
|
+
This allows a flatter, more convenient project folder structure, since the deployment descriptor maps
|
191
|
+
the files into their proper operating system locations.
|
192
|
+
|
193
|
+
As for Folders, the Files element can define the default Owner, Group and Mode for all File children.
|
194
|
+
|
195
|
+
=== +Allfiles+
|
196
|
+
A convenient shorthand for copying all the files in a folder to another folder is the
|
197
|
+
+Allfiles+ element. In this example, we will copy everything from the conf folder to
|
198
|
+
the destination folder specified.
|
199
|
+
<Allfiles Group="bin" Mode="0644" Owner="root" Source="conf">/var/axonsec/conf</Allfiles>
|
200
|
+
|
201
|
+
=== +Daemon+
|
202
|
+
To enable an application to startup automatically, a +Daemon+ element is needed. The name of the
|
203
|
+
startup script is expected to be <code>/etc/rc.d/myapp</code>.
|
204
|
+
<Daemon Flags="" Priority="10" User="" />
|
205
|
+
|
206
|
+
The +Flags+ attribute contains the command line options provided to the executable by
|
207
|
+
<code>/etc/rc.conf.local</code>.
|
208
|
+
|
209
|
+
The lower the +Priority+ value, the earlier that script is run (high values start later).
|
210
|
+
|
211
|
+
The +User+ is the user running the process, typically an unprivileged user named <code>_myapp</code>.
|
212
|
+
Leave blank to run the startup script as root (eg. as when dropping privileges).
|
213
|
+
|
214
|
+
=== +Crontab+
|
215
|
+
Cron jobs are often needed for an application to run smoothly. Instead of creating them by hand,
|
216
|
+
you can specify each job as part of the deployment descriptor and let promotion set up the crontab
|
217
|
+
automatically.
|
218
|
+
<Crontab>
|
219
|
+
<Schedule User="root" Hour="2" Minute="7"
|
220
|
+
Comment="Backup the entire database at 2:07am each morning">
|
221
|
+
<Command><![CDATA[/usr/local/sbin/myappbak]]></Command>
|
222
|
+
</Schedule>
|
223
|
+
</Crontab>
|
224
|
+
In this example, we specify a job to be added to root's crontab, to backup the database
|
225
|
+
at 2:07am each morning. The time specification is as defined in <code>crontab(5)</code>:
|
226
|
+
- Minute
|
227
|
+
- Hour
|
228
|
+
- DayOfMonth
|
229
|
+
- Month
|
230
|
+
- DayOfWeek
|
231
|
+
The +Command+ is best wrapped safely in a CDATA section in case of XML-unsafe characters
|
232
|
+
like $.
|
233
|
+
|
234
|
+
The +Comment+ will be inserted into the final crontab file just before the job specification.
|
235
|
+
|
236
|
+
=== +Database+
|
237
|
+
If the application has a database, we need to specify the path to the DBMS client command
|
238
|
+
line tool.
|
239
|
+
<Database>/usr/local/bin/mysql</Database>
|
240
|
+
|
241
|
+
If using SQLite3, a +Database+ attribute is also needed to specify the file to operate on:
|
242
|
+
<Database database="/var/myapp/data/orders.db">/usr/bin/sqlite3</Database>
|
243
|
+
|
244
|
+
== Database schema migration
|
245
|
+
Four components are required to automate database schema migration:
|
246
|
+
1. Database migration scripts, stored in the +evolve+ and +devolve+ sub-folders
|
247
|
+
of the application's staging folder. These are normal migration scripts you might apply
|
248
|
+
manually.
|
249
|
+
2. A <code><Database></code> element in the deployment descriptor, containing the path to
|
250
|
+
the database client command line tool. In the case of SQLite3, it also needs a <code>database</code>
|
251
|
+
attribute containing the path of the database file to operate on.
|
252
|
+
3. Privileges to allow the user to apply the migration scripts to the database, or else
|
253
|
+
it will of course fail.
|
254
|
+
4. Credentials for the user stored privately in <code>~/.my.cnf</code> or <code>~/.pgpass</code>
|
255
|
+
(unless using SQLite3). This allows the scripts to be executed in batch mode, without
|
256
|
+
prompting for a password.
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.7
|
data/bin/devolve
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/local/bin/ruby -I/usr/local/
|
2
|
+
# This script devolves the database
|
3
|
+
|
4
|
+
require 'getoptlong'
|
5
|
+
#______________________
|
6
|
+
# Set usage
|
7
|
+
#
|
8
|
+
usage=<<-EOT
|
9
|
+
devolve - devolve the database schema using migration scripts
|
10
|
+
usage: sudo /usr/local/bin/devolve [--config configFilepath] appname target
|
11
|
+
where appname is the name of a folder in /var/staging containing a file deploy.xml
|
12
|
+
target integer version to devolve to
|
13
|
+
EOT
|
14
|
+
#______________________
|
15
|
+
# parse options
|
16
|
+
#
|
17
|
+
opts = GetoptLong.new(
|
18
|
+
[ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
|
19
|
+
[ "--help", "-h", GetoptLong::NO_ARGUMENT ]
|
20
|
+
)
|
21
|
+
config = nil
|
22
|
+
configFilename = nil
|
23
|
+
opts.each do |opt, arg|
|
24
|
+
case opt
|
25
|
+
when "--config"
|
26
|
+
configFilename = arg
|
27
|
+
when "--help"
|
28
|
+
puts usage
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
#______________________
|
33
|
+
# check arguments
|
34
|
+
#
|
35
|
+
if ARGV.length != 2
|
36
|
+
puts(usage)
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
appname = ARGV[0]
|
40
|
+
targetVersion = ARGV[1].to_i()
|
41
|
+
unless targetVersion >= 1000
|
42
|
+
puts("target version must be 1000 or greater")
|
43
|
+
puts(usage)
|
44
|
+
exit 1
|
45
|
+
end
|
46
|
+
configFilename ||= "promotion/config.rb"
|
47
|
+
require(configFilename)
|
48
|
+
unless File.directory?(File.expand_path(appname, Folders::Staging))
|
49
|
+
puts("The application #{appname} was not found in the staging area #{Folders::Staging}")
|
50
|
+
puts("Aborting")
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
#______________________
|
54
|
+
# start application
|
55
|
+
#
|
56
|
+
$stdout.flush()
|
57
|
+
require 'promotion/application'
|
58
|
+
app = Promotion::Application.new(appname)
|
59
|
+
app.devolve(targetVersion)
|
data/bin/evolve
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/usr/local/bin/ruby -I/usr/local/
|
2
|
+
# This script evolves the database
|
3
|
+
|
4
|
+
require 'getoptlong'
|
5
|
+
#______________________
|
6
|
+
# Set usage
|
7
|
+
#
|
8
|
+
usage=<<-EOT
|
9
|
+
evolve - evolve the database schema using migration scripts
|
10
|
+
usage: sudo /usr/local/bin/evolve [--config configFilepath] appname [target]
|
11
|
+
where appname is the name of a folder in /var/staging containing a file deploy.xml
|
12
|
+
target is an integer version to evolve to (default is to evolve as far as possible)
|
13
|
+
EOT
|
14
|
+
#______________________
|
15
|
+
# parse options
|
16
|
+
#
|
17
|
+
opts = GetoptLong.new(
|
18
|
+
[ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
|
19
|
+
[ "--help", "-h", GetoptLong::NO_ARGUMENT ]
|
20
|
+
)
|
21
|
+
config = nil
|
22
|
+
configFilename = nil
|
23
|
+
opts.each do |opt, arg|
|
24
|
+
case opt
|
25
|
+
when "--config"
|
26
|
+
configFilename = arg
|
27
|
+
when "--help"
|
28
|
+
puts usage
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
#______________________
|
33
|
+
# check arguments
|
34
|
+
#
|
35
|
+
if ARGV.length < 1
|
36
|
+
puts(usage)
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
appname = ARGV[0]
|
40
|
+
if ARGV.length > 1
|
41
|
+
targetVersion = ARGV[1].to_i()
|
42
|
+
unless targetVersion > 1000
|
43
|
+
puts(usage)
|
44
|
+
exit 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
configFilename ||= "promotion/config.rb"
|
48
|
+
require(configFilename)
|
49
|
+
unless File.directory?(File.expand_path(appname, Folders::Staging))
|
50
|
+
puts("The application #{appname} was not found in the staging area #{Folders::Staging}")
|
51
|
+
puts("Aborting")
|
52
|
+
exit 1
|
53
|
+
end
|
54
|
+
#______________________
|
55
|
+
# start application
|
56
|
+
#
|
57
|
+
$stdout.flush()
|
58
|
+
require 'promotion/application'
|
59
|
+
app = Promotion::Application.new(appname)
|
60
|
+
app.evolve(targetVersion)
|
data/bin/promote
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/local/bin/ruby -I/usr/local/
|
2
|
+
# This script starts the promotion application
|
3
|
+
|
4
|
+
#______________________
|
5
|
+
if Process.euid != 0
|
6
|
+
puts("This script must be run as root")
|
7
|
+
exit 1
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'getoptlong'
|
11
|
+
#______________________
|
12
|
+
# Set usage
|
13
|
+
#
|
14
|
+
usage=<<-EOT
|
15
|
+
promotion - installer and configuration manager
|
16
|
+
usage: sudo /usr/local/bin/promotion [--config configFilepath] appname
|
17
|
+
where appname is the name of a folder under /var/staging
|
18
|
+
EOT
|
19
|
+
#______________________
|
20
|
+
# parse options
|
21
|
+
#
|
22
|
+
opts = GetoptLong.new(
|
23
|
+
[ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
|
24
|
+
[ "--verbose", "-v", GetoptLong::NO_ARGUMENT ],
|
25
|
+
[ "--help", "-h", GetoptLong::NO_ARGUMENT ]
|
26
|
+
)
|
27
|
+
config = nil
|
28
|
+
configFilename = nil
|
29
|
+
opts.each do |opt, arg|
|
30
|
+
case opt
|
31
|
+
when "--config"
|
32
|
+
configFilename = arg
|
33
|
+
when "--help"
|
34
|
+
puts usage
|
35
|
+
exit 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
#______________________
|
39
|
+
# check arguments
|
40
|
+
#
|
41
|
+
if ARGV.length() == 0
|
42
|
+
puts("No application to install. We will just regenerate the environment files.")
|
43
|
+
elsif ARGV.length() > 1
|
44
|
+
puts("Please specify just one application at a time.")
|
45
|
+
puts(usage)
|
46
|
+
exit 1
|
47
|
+
end
|
48
|
+
appname = ARGV[0]
|
49
|
+
unless File.directory?(File.expand_path(appname, "/var/staging"))
|
50
|
+
puts("The application #{appname} was not found in the staging area /var/staging")
|
51
|
+
puts("Aborting")
|
52
|
+
exit 1
|
53
|
+
end
|
54
|
+
#______________________
|
55
|
+
# Configure
|
56
|
+
#
|
57
|
+
configFilename ||= "promotion/config.rb"
|
58
|
+
require(configFilename)
|
59
|
+
#______________________
|
60
|
+
# start application
|
61
|
+
#
|
62
|
+
print("Promoting #{appname}...")
|
63
|
+
$stdout.flush()
|
64
|
+
require 'promotion/application'
|
65
|
+
app = Promotion::Application.new(appname)
|
66
|
+
app.promote()
|
67
|
+
puts("OK.")
|
data/deploy.xml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
2
|
+
<Specification Name="promotion" Title="Promotion Manager"
|
3
|
+
xmlns="http://finalstep.com.au/promotion/v100"
|
4
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
5
|
+
xsi:schemaLocation="http://finalstep.com.au/schema/promotion.v100.xsd ../promotion/promotion.xsd">
|
6
|
+
<Description>
|
7
|
+
The promotion manager ensures that the environment for an application
|
8
|
+
is set up correctly, including folder and file permissions and ownership,
|
9
|
+
startup scripts, sudoers privileges and environment variables.
|
10
|
+
</Description>
|
11
|
+
<Folders>
|
12
|
+
<Folder Mode="0770">/var/staging</Folder>
|
13
|
+
<Folder Mode="0770">/var/staging/promotion</Folder>
|
14
|
+
<Folder Clear="true">/usr/local/promotion</Folder>
|
15
|
+
<Folder>/usr/local/promotion/erb</Folder>
|
16
|
+
</Folders>
|
17
|
+
<Files Source="bin" Mode="0755">
|
18
|
+
<File>/usr/local/sbin/promote</File>
|
19
|
+
<File>/usr/local/sbin/evolve</File>
|
20
|
+
<File>/usr/local/sbin/devolve</File>
|
21
|
+
</Files>
|
22
|
+
<Files Source="lib/promotion">
|
23
|
+
<File>/usr/local/promotion/application.rb</File>
|
24
|
+
<File>/usr/local/promotion/config.rb</File>
|
25
|
+
<File>/usr/local/promotion/enforcer.rb</File>
|
26
|
+
<File>/usr/local/promotion/generator.rb</File>
|
27
|
+
<File>/usr/local/promotion/evolver.rb</File>
|
28
|
+
</Files>
|
29
|
+
<Files Source="lib/promotion/erb">
|
30
|
+
<File>/usr/local/promotion/erb/profile.erb</File>
|
31
|
+
<File>/usr/local/promotion/erb/rc.conf.local.erb</File>
|
32
|
+
<File>/usr/local/promotion/erb/sudoers.erb</File>
|
33
|
+
<File>/usr/local/promotion/erb/crontab.erb</File>
|
34
|
+
</Files>
|
35
|
+
</Specification>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
# Working directory is the ext/promotion/ folder
|
3
|
+
|
4
|
+
# generate Makefile to install binaries
|
5
|
+
source = "../../bin"
|
6
|
+
target = "/usr/local"
|
7
|
+
makeContents=<<-EOT
|
8
|
+
# Makefile for Promotion
|
9
|
+
.PHONY : all
|
10
|
+
all :
|
11
|
+
.PHONY : install
|
12
|
+
install :
|
13
|
+
install -m 0750 -o root -g wheel #{source}/promote #{target}/sbin
|
14
|
+
install -m 0750 -o root -g wheel #{source}/evolve #{target}/sbin
|
15
|
+
install -m 0750 -o root -g wheel #{source}/devolve #{target}/sbin
|
16
|
+
EOT
|
17
|
+
makefile = File.new("Makefile", "w")
|
18
|
+
makefile.puts(makeContents)
|
19
|
+
makefile.close()
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'promotion/enforcer'
|
3
|
+
require 'promotion/generator'
|
4
|
+
require 'promotion/evolver'
|
5
|
+
require 'log4r'
|
6
|
+
include Log4r
|
7
|
+
|
8
|
+
module Promotion # :nodoc:
|
9
|
+
|
10
|
+
# The Promotion::Application coordinates the activity of the components:
|
11
|
+
# - Enforcer
|
12
|
+
# - Generator
|
13
|
+
# - Evolver
|
14
|
+
class Application
|
15
|
+
|
16
|
+
# Creates a new Promotion::Application instance for a specific app to be promoted.
|
17
|
+
# A global +$log+ logger is also created.
|
18
|
+
def initialize(appname)
|
19
|
+
@appname = appname
|
20
|
+
$log = Logger.new(@appname)
|
21
|
+
FileOutputter[@appname] = FileOutputter.new(@appname, :filename => Files::Log)
|
22
|
+
FileOutputter[@appname].formatter = PatternFormatter.new(:pattern => "%d %m")
|
23
|
+
$log.outputters = FileOutputter[@appname]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Promotes an application into production.
|
27
|
+
def promote()
|
28
|
+
enforcer = Promotion::Enforcer.new(@appname)
|
29
|
+
generator = Promotion::Generator.new()
|
30
|
+
enforcer.start()
|
31
|
+
generator.start()
|
32
|
+
$log.info("Application #{@appname} successfully promoted.")
|
33
|
+
end
|
34
|
+
|
35
|
+
# Evolves the database to a target version, or as far as possible, by
|
36
|
+
# executing database migration scripts in the +evolve+ folder
|
37
|
+
def evolve(target=nil)
|
38
|
+
evolver = Promotion::Evolver.new(@appname, true, target)
|
39
|
+
evolver.start()
|
40
|
+
end
|
41
|
+
|
42
|
+
# Devolve a database back to an earlier version
|
43
|
+
def devolve(target=nil)
|
44
|
+
evolver = Promotion::Evolver.new(@appname, false, target)
|
45
|
+
evolver.start()
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# application configuration for promotion
|
2
|
+
# PRODUCTION
|
3
|
+
|
4
|
+
# A section of the configuration file that contains folder paths
|
5
|
+
module Folders
|
6
|
+
# The folder where applications are staged (eg. from Subversion)
|
7
|
+
# ready for deployment
|
8
|
+
Staging = "/var/staging"
|
9
|
+
# The folder containing the ERB templates
|
10
|
+
Templates = "/usr/local/promotion/erb"
|
11
|
+
# The system crontab folder
|
12
|
+
Crontabs = "/var/cron/tabs"
|
13
|
+
end
|
14
|
+
|
15
|
+
# A section of the configuration file that contains file paths
|
16
|
+
module Files
|
17
|
+
# The standard name of the deployment descriptor for all apps
|
18
|
+
Spec = "deploy.xml"
|
19
|
+
# The log file for promotion
|
20
|
+
Log = "/var/staging/Promotion.log"
|
21
|
+
# system path to the shared profile
|
22
|
+
Profile = "/etc/profile"
|
23
|
+
# system path to the sudoers file
|
24
|
+
Sudoers = "/etc/sudoers"
|
25
|
+
# system path to rc.conf.local
|
26
|
+
Rc_conf = "/etc/rc.conf.local"
|
27
|
+
# path to useradd executable
|
28
|
+
Useradd = "/usr/sbin/useradd"
|
29
|
+
# path to groupadd executable
|
30
|
+
Groupadd = "/usr/sbin/groupadd"
|
31
|
+
# path to visudo executable
|
32
|
+
Visudo = "/usr/sbin/visudo"
|
33
|
+
# path to crontab executable
|
34
|
+
Crontab = "/usr/bin/crontab"
|
35
|
+
# path to zip executable
|
36
|
+
Zip = "/usr/local/bin/zip -q "
|
37
|
+
end
|
38
|
+
|
39
|
+
# A section of the configuration file that contains ERB template filenames
|
40
|
+
module Templates
|
41
|
+
# filename for Profile template
|
42
|
+
Profile = "profile.erb"
|
43
|
+
# filename for Rc_conf template
|
44
|
+
Rc_conf = "rc.conf.local.erb"
|
45
|
+
# filename for Sudoers template
|
46
|
+
Sudoers = "sudoers.erb"
|
47
|
+
# filename for Crontab template
|
48
|
+
Crontab = "crontab.erb"
|
49
|
+
end
|
50
|
+
|
51
|
+
raise "staging area not found" unless File.directory?(Folders::Staging)
|
52
|
+
raise "template folder not found" unless File.directory?(Folders::Templates)
|
53
|
+
Templates.constants { |template|
|
54
|
+
raise "template #{template} not found" unless File.exist?(File.expand_path(Templates.const_get(template), Folders::Templates))
|
55
|
+
}
|