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
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
|
+
}
|