stack-kicker 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
data/.rvmrc CHANGED
@@ -6,7 +6,7 @@
6
6
  # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
7
7
  # Only full ruby name is supported here, for short names use:
8
8
  # echo "rvm use 1.9.3" > .rvmrc
9
- environment_id="ruby-1.9.3-p286@stack-kicker"
9
+ environment_id="ruby-1.9.3@stack-kicker"
10
10
 
11
11
  # Uncomment the following lines if you want to verify rvm version per project
12
12
  # rvmrc_rvm_version="1.16.17 (stable)" # 1.10.1 seams as a safe start
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ ## 0.0.11
2
+ Stack.validate() was still using the role name, not role_details[:security_group]
3
+ Added doc/examples/apache-via-cloud-init, example of just using cloud-init, no chef.
4
+ Fixed some issues when only using cloud-init
data/README.md CHANGED
@@ -1,14 +1,10 @@
1
1
  # stack-kicker
2
2
 
3
- stack-kicker is a simple 'application stack' deployment tool, it's purpose in life
4
- is to spin up a set of instances in a repeatable, controlled fashion, and optionally
3
+ stack-kicker is a simple 'application stack' deployment tool, it's purpose in life
4
+ is to spin up a set of instances in a repeatable, controlled fashion, and optionally
5
5
  run post-install scripts after each instance has been started.
6
6
 
7
- stack-kicker has hooks to allow default & custom cloud-init templates to be built & passed to
8
- your compute provider (we currently use ruby-openstack, so are limited to OpenStack providers,
9
- however, a sister project, aws-kicker, uses fog.io, the interaction with the compute provider is
10
- minimal, so it's on the roadmap to merge aws-kicker & stack-kicker, and use either an internal
11
- abstraction layer or just fog.io for all compute provisioning requests)
7
+ stack-kicker has hooks to allow default & custom cloud-init templates to be built & passed to your compute provider (we currently use ruby-openstack, so are limited to OpenStack providers, however, a sister project, aws-kicker, uses fog.io, the interaction with the compute provider is minimal, so it's on the roadmap to merge aws-kicker & stack-kicker, and use either an internal abstraction layer or just fog.io for all compute provisioning requests)
12
8
 
13
9
  ## Stackfile
14
10
  Normally, stack configurations are stored in a Stackfile, which is a ruby hash of configuration options.
@@ -24,14 +20,72 @@ stack-kicker sequentially iterates over defined roles, creating the required num
24
20
  Hostnames are generated from a customizable template, which is effectively:
25
21
 
26
22
  config[:name_template] = '%s-%s-%s%04d'
27
- config['global_service_name'] = 'myapp'
23
+ config[:global_service_name] = 'myapp'
28
24
  site = <derived from region/az, via config[:site_template]>
29
- hostname = sprintf(config[:name_template], config['global_service_name'], site, role, position)
25
+ hostname = sprintf(config[:name_template], config[:global_service_name], site, role, position)
30
26
 
31
- So hostnames will be myapp-az1-chef0001, myapp-az1-web0001, myapp-az1-web0002 etc.
27
+ So hostnames will be myapp-az1-chef0001, myapp-az1-web0001, myapp-az1-web0002 etc.
32
28
 
33
29
  post-install scripts are executed from the same host as stack-kicker is being used, using the same credentials as the current user. They are can be used to retrieve information from a freshly built node (like certificates from a chef server), so block progress until the chef-client run has completed (we use this to block percona/galera & rabbitmq cluster builds so that the first node is up & running correctly before we try and add another node to the cluster)
34
30
 
31
+ ### [Role Attributes](id:role_attributes)
32
+ Roles have several attributes, which control how & how many nodes are created, and how they are created. Below shows the default values for these attributes, which can all be overridden.
33
+
34
+ :role_name = {
35
+ :count => 1,
36
+ :azs[] => config['REGION']
37
+
38
+ :chef_server => false,
39
+ :skip_chef_prereg => false,
40
+ :security_group => :role_name.to_s,
41
+
42
+ :cloud_config_yaml => 'cloud-config.yaml',
43
+ :bootstrap => 'chef-client-bootstrap-excl-validation-pem.sh',
44
+ :data_dir => '/dummy',
45
+
46
+ :floating_ips => nil
47
+
48
+ :post_install_script => nil,
49
+ :post_install_args => '',
50
+ :post_install_cwd => '/.'
51
+ }
52
+
53
+ #### :count
54
+ The number of nodes or instances of this role that will be created.
55
+ #### :azs
56
+ This can be an array of strings, so that nodes will be placed in specific availability zones. If no array is set, the REGION set in the global section will be used.
57
+ #### :chef_server
58
+ this is flag used to denote if the node created by this role should be used as a chef server for nodes created by other roles. If this flag is set, we extract the public & private IP address for use later, as well downloading validation.pem & creating a user account in chef & downloading the pen (this is done via chef-post-install.sh, or your own alternative methods)
59
+ #### :skip_chef_prereg
60
+ This is usually only used when :chef_server = true, it stops stack-kicker from attempting to pre-create the chef client & node and applying roles to the node.
61
+ #### :security_group
62
+ security group to be assigned to this node. (set to default is you don't want to manage security groups for every role)
63
+ #### :cloud_config_yaml
64
+ defaults to a file which contains a simple template (lib/cloud-config.yaml in the github repo) that installs the http://apt.opscode.com repo & gig key, as well as installing the opscode-keyring. Can be replaced with any filename that complies with cloud-init.
65
+ #### :bootstrap
66
+ Optional filename, the contents of which will get combined with :cloud_config_yaml to form the cloud-init payload (using mime encoding, supported types are #include, ) with some variable substation (chef server ip, environment, validation.pem, roles) See lib/chef-client-bootstrap-excl-validation-pem.sh as an example.
67
+
68
+ :cloud_config_yaml & :bootstrap files can be of the following type:
69
+
70
+ mime-type | first line
71
+ -------------------|-----------
72
+ text/x-include-url | #include
73
+ text/x-shellscript | #!
74
+ text/cloud-config | #cloud-config
75
+ text/upstart-job | #upstart-job
76
+ text/part-handler | #part-handler
77
+ text/cloud-boothook | #cloud-boothook
78
+
79
+ #### :data_dir
80
+ data_dir is a hook into the optional :cloud_config_yaml template (lib/cloud-config-w-ephemeral.yaml), which formats & mounts ephemeral0 early in the boot process, allowing it to be used during the rest of the cloud-init. ephemeral0 is mounted as /mnt & then bind mounted to #{data_dir}
81
+
82
+ #### :floating_ips
83
+ This can be an array of strings, such that node X will be assigned :floating_ips[X-1] via "nova add-floating-ip". If :floating_ips[X-1].nil?, then no floating ip will be attached. (the floating ip must already be assigned to a pool in your account)
84
+
85
+ #### :post_install_script, :post_install_args & :post_install_cwd
86
+ These are used to construct a command to execute, which is executed locally where you executed stack-kicker. :post_install_args can contain %PUBLIC_IP%, which will be replaced by the public IP of the just created node. :post_install_script scripts are executed as soon the the instance returns a status='ACTIVE'. They can be used delay the creation of further nodes of the same role (for example, when creating a rabbitmq cluster, you need to wait for the rabbitmq process to be running before creating the next member of the cluster, or when you are creating a chef-server, you need to wait for the packages to install & daemons to start before attempting to create Chef users & retrieve keys)
87
+
88
+
35
89
  ## Example workflows/models
36
90
  stack-kicker was built with the following workflows in mind:
37
91
 
@@ -39,11 +93,11 @@ stack-kicker was built with the following workflows in mind:
39
93
  This was the original requirement, a multi-role application stack build that started
40
94
  with building a chef-server, uploading roles, environments, cookbooks & databags to it,
41
95
  and then building the rest of the application-stack instances, using the freshly built chef-server
42
- to drop the application on to the instances. In this setup we used vanilla images (Ubuntu 12.04 LTS,
96
+ to drop the application on to the instances. In this setup we used vanilla images (Ubuntu 12.04 LTS,
43
97
  but you could use any image, either vanilla or pre-populated with your software).
44
98
 
45
99
  Here's an example Stackfile for this:
46
-
100
+
47
101
  module StackConfig
48
102
  Stacks = {
49
103
  'web-w-chef-server' => {
@@ -53,14 +107,14 @@ Here's an example Stackfile for this:
53
107
  'PASSWORD' => ENV['OS_PASSWORD'],
54
108
  'AUTH_URL' => ENV['OS_AUTH_URL'],
55
109
  'TENANT_NAME' => ENV['OS_TENANT_NAME'],
56
-
110
+
57
111
  # generic instance info
58
112
  'flavor_id' => 103,
59
113
  'image_id' => 75845,
60
114
  :key_pair => 'ssh-keypair-name',
61
115
  :key_public => '/path/to/id_rsa.pub',
62
116
  :global_service_name => 'perconaconf',
63
-
117
+
64
118
  # role details
65
119
  :roles => {
66
120
  # override the default cloud-init script & default bootstrap (which is a chef-client bootstrap)
@@ -70,13 +124,13 @@ Here's an example Stackfile for this:
70
124
  # override the default cloud-config with a chef-server template
71
125
  :cloud_config_yaml => 'chef-cloud-config.yaml',
72
126
  # skip the default chef-client bootstrap
73
- :bootstrap => '',
127
+ :bootstrap => '',
74
128
  # wait for the chef server to come up & download pem files & generate client account
75
129
  :post_install_script => 'bootstrap/chef-post-install.sh',
76
- # our post install script dumps out .pem files in the CWD
77
- :post_install_cwd => '.chef',
130
+ # our post install script dumps out .pem files in the CWD
131
+ :post_install_cwd => '.chef',
78
132
  # The post-install script needs to know the public IP of the just built instance so that this station can access it
79
- :post_install_args => '%PUBLIC_IP%'
133
+ :post_install_args => '%PUBLIC_IP%'
80
134
  },
81
135
  # much simpler role, just build 3 of these, chef-client will do the rest on boot
82
136
  :web => { :count => 3 }
@@ -84,34 +138,34 @@ Here's an example Stackfile for this:
84
138
  }
85
139
  }
86
140
  end
87
-
141
+
88
142
 
89
143
  ### simple roles
90
- There is no requirement that stack-kicker do anything other than spin up your instances, your requirements
144
+ There is no requirement that stack-kicker do anything other than spin up your instances, your requirements
91
145
  may be such that you just need a number of instances started with certain images, region & flavor requirements.
92
146
 
93
147
  ### masterless puppet
94
- aws-kicker (a sister project) had an original requirement of starting a simple 2-tier web application in multiple
95
- locations/environments (prod, stage, dev etc), to do this we configured the instances by bootrapping puppet,
148
+ aws-kicker (a sister project) had an original requirement of starting a simple 2-tier web application in multiple
149
+ locations/environments (prod, stage, dev etc), to do this we configured the instances by bootrapping puppet,
96
150
  git clonig /etc/puppet and running "puppet apply", a simple pattern used in many places, this was all achievd with a
97
151
  carefully crafted cloud-init template (incidentally, this also allowed for simple prototyping using vagrant to
98
152
  provide local instances using the exact same '/etc/puppet' git repo.
99
153
 
100
154
  ### Other workflows
101
- These are only the workflows I've used, there is no reason a puppet master couldn't be built & used, or
155
+ These are only the workflows I've used, there is no reason a puppet master couldn't be built & used, or
102
156
  hosted/external puppet & chef servers. (pull requests accepted etc, including salt, ansible, cfengine etc..)
103
157
 
104
158
  ## Installation
105
159
 
106
160
  $ gem install stack-kicker
107
-
161
+
108
162
  ## Requirements
109
163
  In addition to the the ruby dependencies which gem will install for you, access to python-novaclient is currently required to attach floating-ips to instances.
110
164
 
111
165
  ## Usage
112
166
 
113
167
  Usage: stack-kicker [options] task
114
-
168
+
115
169
  Options:
116
170
  -h, --help Show command line help
117
171
  --stackfile Stackfile Specify an alternative Stackfile
@@ -122,7 +176,7 @@ In addition to the the ruby dependencies which gem will install for you, access
122
176
  --log-level LEVEL Set the logging level
123
177
  (debug|info|warn|error|fatal)
124
178
  (Default: info)
125
-
179
+
126
180
  Arguments:
127
181
 
128
182
  task
@@ -130,13 +184,13 @@ In addition to the the ruby dependencies which gem will install for you, access
130
184
 
131
185
  ## TODO
132
186
 
133
- 1. Clean up provider logic
187
+ 1. Clean/Add up provider logic
134
188
  2. Remove dependency on python-novaclient for floating-ip attach
135
189
  3. Remove dependency on a full chef gem install
136
190
  4. Better docs & examples
137
191
  5. Support for AWS EC2 (from aws-kicker)
138
192
  5. Support for DNS Updates on instance creation (from aws-kicker)
139
- 6.
193
+
140
194
  ## Contributing
141
195
 
142
196
  1. Fork it
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This is an RVM Project .rvmrc file, used to automatically load the ruby
4
+ # development environment upon cd'ing into the directory
5
+
6
+ # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
7
+ # Only full ruby name is supported here, for short names use:
8
+ # echo "rvm use 1.9.3" > .rvmrc
9
+ environment_id="ruby-1.9.3-p327@stack-kicker-examples"
10
+
11
+ # Uncomment the following lines if you want to verify rvm version per project
12
+ # rvmrc_rvm_version="1.17.3 (stable)" # 1.10.1 seams as a safe start
13
+ # eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
14
+ # echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
15
+ # return 1
16
+ # }
17
+
18
+ # First we attempt to load the desired environment directly from the environment
19
+ # file. This is very fast and efficient compared to running through the entire
20
+ # CLI and selector. If you want feedback on which environment was used then
21
+ # insert the word 'use' after --create as this triggers verbose mode.
22
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
23
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
24
+ then
25
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
26
+ [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
27
+ \. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
28
+ else
29
+ # If the environment file has not yet been created, use the RVM CLI to select.
30
+ rvm --create "$environment_id" || {
31
+ echo "Failed to create RVM environment '${environment_id}'."
32
+ return 1
33
+ }
34
+ fi
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ # source 'http://15.185.118.80/partial-gem-mirror/'
3
+
4
+ gem 'stack-kicker', '>=0.0.10'
@@ -0,0 +1,40 @@
1
+ module StackConfig
2
+ Stacks = Hash.new
3
+
4
+ Stacks['apache-cloud-init'] = {
5
+ # (we can access environment variable via ENV['foo'] instead of hard coding u/p here)
6
+ 'REGION' => ENV['OS_REGION_NAME'],
7
+ 'USERNAME' => ENV['OS_USERNAME'],
8
+ 'PASSWORD' => ENV['OS_PASSWORD'],
9
+ 'AUTH_URL' => ENV['OS_AUTH_URL'],
10
+ 'TENANT_NAME' => ENV['OS_TENANT_NAME'],
11
+
12
+ # generic instance info
13
+ 'flavor_id' => 103,
14
+ 'image_id' => 75845, # Ubuntu Precise 12.04 LTS Server 64-bit
15
+ # per-az image_id's
16
+ 'az-2.region-a.geo-1' => { 'image_id' => 67074 },
17
+
18
+ # provisioning info
19
+ :key_pair => 'dnsaas-dev@hp.com',
20
+ :key_public => '../credentials/ssh-keys/dnsaas-dev@hp.com.pub',
21
+
22
+ :name_template => '%s-%s-%s%04d', # service-site-role0001
23
+ :global_service_name => 'webci',
24
+ :site_template => '%s',
25
+
26
+ # role specification
27
+ # role names & chef roles should match
28
+ :roles => {
29
+ :web => { :count => 1,
30
+ # use the default security group
31
+ :security_group => 'default',
32
+ # don't use chef as the second-stage provisioner
33
+ :skip_chef_prereg => true,
34
+ :bootstrap => 'cloud-init.sh',
35
+ # use the yaml template that supports configuring the ephemeral space early in the boot process
36
+ :cloud_config_yaml => 'cloud-init.yaml',
37
+ }
38
+ }
39
+ }
40
+ end
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ cat > /var/www/index.html <<HEREDOC
4
+ <html><body><h1>Stack-Kicker was 'ere!</h1>
5
+ <p>This file is dropped in place by a shell script passed to the host via cloud-init</p>
6
+ <p>The web server software is running but no content has been added, yet.</p>
7
+ </body></html>
8
+ HEREDOC
@@ -0,0 +1,28 @@
1
+ #cloud-config
2
+
3
+ output:
4
+ all: ">> /var/log/cloud-init.log"
5
+
6
+ # use the HPCS Ubuntu Mirror for security-updates too
7
+ bootcmd:
8
+ - echo 127.0.1.1 %HOSTNAME% >> /etc/hosts
9
+ - echo # force security.ubuntu.com to mirror.clouds.archive.ubuntu.com >> /etc/hosts
10
+ - echo 15.185.107.200 security.ubuntu.com >> /etc/hosts
11
+
12
+ # use the HPCS Ubuntu Mirrors
13
+ apt_mirror: http://nova.clouds.archive.ubuntu.com/ubuntu
14
+
15
+ # Run apt-get update
16
+ package_update: true
17
+
18
+ # Run apt-get update
19
+ package_upgrade: true
20
+
21
+ # Install some packages
22
+ packages:
23
+ - apache2
24
+ - php5
25
+
26
+ # Use `sudo -i` to simulate a login shell...
27
+ runcmd:
28
+ - sudo -i touch /var/log/cloud-init.complete
@@ -1,5 +1,5 @@
1
1
  module Stack
2
2
  module Kicker
3
- VERSION = "0.0.10"
3
+ VERSION = "0.0.11"
4
4
  end
5
5
  end
data/lib/stack.rb CHANGED
@@ -28,10 +28,10 @@ require 'tempfile'
28
28
 
29
29
  #
30
30
  # This really needs to be converted into a class....
31
- #
31
+ #
32
32
  module Stack
33
33
 
34
- # Shadow the global constant Logger with Stack::Logger
34
+ # Shadow the global constant Logger with Stack::Logger
35
35
  # (if you want access to the global constant, use ::Logger from inside the Stack module)
36
36
  Logger = Logger.new(STDOUT)
37
37
  Logger.level = ::Logger::INFO
@@ -60,7 +60,7 @@ module Stack
60
60
  Logger.info { " #{name}" }
61
61
  end
62
62
  end
63
-
63
+
64
64
  def Stack.show_stack(config)
65
65
  # syntax_check is a light weight check that doesn't talk to OpenStalk
66
66
  Stack.syntax_check(config)
@@ -76,7 +76,7 @@ module Stack
76
76
  eval(config_raw)
77
77
 
78
78
  # if there is only one stack defined in the Stackfile, load it:
79
- if StackConfig::Stacks.count == 1 && stack_name.nil?
79
+ if StackConfig::Stacks.count == 1 && stack_name.nil?
80
80
  stack_name = StackConfig::Stacks.keys[0]
81
81
  Logger.info { "Defaulting to #{stack_name} as there is a single stack defined and no stack named" }
82
82
  end
@@ -129,9 +129,9 @@ module Stack
129
129
 
130
130
  # check that all the required config items are set
131
131
  def Stack.syntax_check(config)
132
- if config['REGION'].nil? || config['USERNAME'].nil? || config['PASSWORD'].nil? || config['AUTH_URL'].nil? || config['TENANT_NAME'].nil? &&
132
+ if config['REGION'].nil? || config['USERNAME'].nil? || config['PASSWORD'].nil? || config['AUTH_URL'].nil? || config['TENANT_NAME'].nil? &&
133
133
  config['REGION'].empty? || config['USERNAME'].empty? || config['PASSWORD'].empty? || config['AUTH_URL'].empty? || config['TENANT_NAME'].empty?
134
- Logger.error { "REGION, USERNAME, PASSWORD, AUTH_URL & TENANT_NAME must all be set" }
134
+ Logger.error { "REGION, USERNAME, PASSWORD, AUTH_URL & TENANT_NAME must all be set" }
135
135
  exit
136
136
  end
137
137
 
@@ -146,7 +146,7 @@ module Stack
146
146
  if !File.directory?(dot_chef_abs)
147
147
  Logger.warn "#{dot_chef_abs} doesn't exist"
148
148
  end
149
-
149
+
150
150
  # Check we have a #{dot_chef_abs}/.chef/knife.rb
151
151
  knife_rb_abs = dot_chef_abs + '/knife.rb'
152
152
  if File.exists?(knife_rb_abs)
@@ -175,7 +175,7 @@ module Stack
175
175
  Logger.error "Couldn't find #{config[:key_pair]} key in the ssh-agent key list! Aborting!"
176
176
  Logger.erroLogger.error "ssh_keys_loaded: #{ssh_keys_loaded}"
177
177
  exit 2
178
- end
178
+ end
179
179
  end
180
180
 
181
181
  # populate the config & then walk through the AZs verifying the config
@@ -199,7 +199,7 @@ module Stack
199
199
  keypairs = os.keypairs()
200
200
  if (keypairs[config[:key_pair]].nil? && keypairs[config[:key_pair].to_sym].nil?)
201
201
  Logger.warn "#{config[:key_pair]} isn't available, uploading the key"
202
-
202
+
203
203
  # upload the key
204
204
  key = os.create_keypair({:name=> config[:key_pair], :public_key=> File.read(config[:key_public])})
205
205
  Logger.warn "#{config[:key_pair]} fingerprint=#{key[:fingerprint]}"
@@ -216,10 +216,10 @@ module Stack
216
216
 
217
217
  config[:roles].each do |role, role_details|
218
218
  # is does the secgroup exist?
219
- if sg_names.include?(role.to_s)
220
- Logger.info "security group #{role} exists in #{az}"
219
+ if sg_names.include?(role_details[:security_group])
220
+ Logger.info "security group #{role_details[:security_group]} exists in #{az}"
221
221
  else
222
- Logger.error "security group #{role} is missing in #{az}"
222
+ Logger.error "security group #{role_details[:security_group]} is missing in #{az}"
223
223
  end
224
224
  end
225
225
  end
@@ -251,7 +251,7 @@ module Stack
251
251
 
252
252
  client_key = dot_chef_abs + '/' + config[:name] + '-' + ENV['USER'] + '.pem'
253
253
  validation_key = dot_chef_abs + '/' + config[:name] + '-' + 'validation.pem'
254
-
254
+
255
255
  Logger.debug "stackhome: #{config[:stackhome]}"
256
256
  Logger.debug "Current user client key: #{client_key}"
257
257
  Logger.debug "New Host Validation key: #{validation_key}"
@@ -290,11 +290,11 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
290
290
  Logger.debug role_details[:azs]
291
291
 
292
292
  site = sprintf(config[:site_template], role_details[:azs][position-1].split('.')[0].sub(/-/, ''))
293
-
293
+
294
294
  # generate the hostname
295
295
  hostname = sprintf(config[:name_template], config[:global_service_name], site, role, position)
296
296
 
297
- hostname
297
+ hostname
298
298
  end
299
299
 
300
300
  def Stack.generate_server_names(config)
@@ -305,7 +305,7 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
305
305
 
306
306
  def Stack.populate_config(config)
307
307
  # config[:role_details] contains built out role details with defaults filled in from stack defaults
308
- # config[:node_details] contains node details built out from role_details
308
+ # config[:node_details] contains node details built out from role_details
309
309
 
310
310
  # set some sensible defaults to the stack-wide defaults if they haven't been set in the Stackfile.
311
311
  if config[:provisioner].nil?
@@ -330,7 +330,7 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
330
330
 
331
331
  if config[:name_template].nil?
332
332
  Logger.warn { "Defaulting to '%s-%s-%s%04d' for config[:name_template]" }
333
- config[:name_template] = '%s-%s-%s%04d'
333
+ config[:name_template] = '%s-%s-%s%04d'
334
334
  end
335
335
 
336
336
  if config[:site_template].nil?
@@ -343,24 +343,24 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
343
343
  config[:site_template] = 'UNKNOWN'
344
344
  end
345
345
 
346
-
346
+
347
347
  if config[:node_details].nil?
348
348
  Logger.debug { "Initializing config[:node_details] and config[:azs]" }
349
349
  config[:node_details] = Hash.new
350
350
  config[:azs] = Array.new
351
351
 
352
- config[:roles].each do |role,role_details|
352
+ config[:roles].each do |role,role_details|
353
353
  Logger.debug { "Setting defaults for #{role}" }
354
-
354
+
355
355
  # default to 1 node of this role if :count isn't set
356
356
  if role_details[:count].nil?
357
357
  role_details[:count] = 1
358
358
  end
359
-
359
+
360
360
  if (role_details[:data_dir].nil?)
361
361
  role_details[:data_dir] = '/dummy'
362
362
  end
363
-
363
+
364
364
  # Has the cloud_config_yaml been overridden?
365
365
  if (role_details[:cloud_config_yaml])
366
366
  role_details[:cloud_config_yaml] = Stack.find_file(config, role_details[:cloud_config_yaml])
@@ -390,24 +390,28 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
390
390
  role_details[:post_install_cwd] = '/.'
391
391
  end
392
392
 
393
+ if role_details[:post_install_args].nil?
394
+ role_details[:post_install_args] = ''
395
+ end
396
+
393
397
  (1..role_details[:count]).each do |p|
394
398
  Logger.debug { "Populating the config[:role_details][:azs] array with AZ" }
395
399
  role_details[:azs] = Array.new if role_details[:azs].nil?
396
-
400
+
397
401
  # is there an az set for this node?
398
402
  if role_details[:azs][p-1].nil?
399
- # inherit the global az
403
+ # inherit the global az
400
404
  Logger.debug { "Inheriting the AZ for #{role} (#{config['REGION']})" }
401
405
  role_details[:azs][p-1] = config['REGION']
402
406
  end
403
-
407
+
404
408
  # add this AZ to the AZ list, we'll dedupe later
405
409
  config[:azs] << role_details[:azs][p-1]
406
-
410
+
407
411
  hostname = Stack.generate_hostname(config, role, p)
408
412
  Logger.debug { "Setting node_details for #{hostname}, using element #{p}-1 from #{role_details[:azs]}" }
409
413
  config[:node_details][hostname] = { :az => role_details[:azs][p-1], :region => role_details[:azs][p-1], :role => role }
410
- end
414
+ end
411
415
  end
412
416
  end
413
417
  config[:azs].uniq!
@@ -429,18 +433,18 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
429
433
  if config[:all_instances].nil? || refresh
430
434
  # we need to get the server list for each AZ mentioned in the config[:roles][:role][:azs], this is populated by Stack.populate_config
431
435
  Stack.populate_config(config)
432
-
436
+
433
437
  # get the current list of servers from OpenStack & generate a hash, keyed on name
434
438
  servers = Hash.new
435
439
  config[:azs].each do |az|
436
440
  os = Stack.connect(config, az)
437
- os.servers.each do |server|
438
- servers[server[:name]] = {
441
+ os.servers.each do |server|
442
+ servers[server[:name]] = {
439
443
  :region => az,
440
- :id => server[:id],
444
+ :id => server[:id],
441
445
  :addresses => os.server(server[:id]).addresses
442
446
  }
443
- end
447
+ end
444
448
  end
445
449
  config[:all_instances] = servers
446
450
  end
@@ -460,9 +464,9 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
460
464
  def Stack.add_instance(config, hostname, region, id, addresses)
461
465
  config[:all_instances][hostname] = { :region => region, :id => id, :addresses => addresses}
462
466
  end
463
-
467
+
464
468
  def Stack.ssh(config, hostname = nil, user = ENV['USER'], command = nil)
465
- # ssh to a host, or all hosts
469
+ # ssh to a host, or all hosts
466
470
 
467
471
  # get all running instances
468
472
  servers = Stack.get_our_instances(config)
@@ -523,10 +527,10 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
523
527
  # this also populates out unspecified defaults, like az
524
528
  Stack.populate_config(config)
525
529
 
526
- # get the list of nodes we consider 'ours', i.e. with hostnames that match
530
+ # get the list of nodes we consider 'ours', i.e. with hostnames that match
527
531
  # those generated by this stack
528
532
  ours = Stack.get_our_instances(config)
529
-
533
+
530
534
  # do any of the list of servers in OpenStack match one of our hostnames?
531
535
  ours.each do |node, node_details|
532
536
  Logger.info "Deleting #{node}"
@@ -535,9 +539,9 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
535
539
  d.delete!
536
540
  end
537
541
  end
538
-
542
+
539
543
  def Stack.get_public_ip(config, hostname)
540
- # get a public address from the instance
544
+ # get a public address from the instance
541
545
  # (could be either the dynamic or one of our floating IPs
542
546
  config[:all_instances][hostname][:addresses].each do |address|
543
547
  if address.label == 'public'
@@ -547,7 +551,7 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
547
551
  end
548
552
 
549
553
  def Stack.set_chef_server(config, chef_server)
550
- # set the private & public URLs for the chef server,
554
+ # set the private & public URLs for the chef server,
551
555
  # called either after we create the Chef Server, or skip over it
552
556
  Logger.debug "Setting :chef_server_hostname, chef_server_private & chef_server_public details (using #{chef_server})"
553
557
 
@@ -575,18 +579,18 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
575
579
  # 2) generate the json to describe that to the "stackhelper secgroup-sync" tool
576
580
  # 3) run "stackhelper secgroup-sync --some-file our-ips.json"
577
581
  ours = Stack.get_our_instances(config)
578
-
582
+
579
583
  secgroup_ips = Hash.new
580
584
  # walk the list of hosts, dumping the IPs into role buckets
581
585
  ours.each do |instance, instance_details|
582
586
  secgroup_ips[instance_details[:role]] = Array.new if secgroup_ips[instance_details[:role]].nil?
583
587
 
584
588
  #secgroup_ips[instance_details[:role]] << instance_details[:addresses].map { |address| address.address }
585
- secgroup_ips[instance_details[:role]] << instance_details[:addresses].map do |address|
586
- if (address.label == 'public')
587
- address.address
588
- else
589
- next
589
+ secgroup_ips[instance_details[:role]] << instance_details[:addresses].map do |address|
590
+ if (address.label == 'public')
591
+ address.address
592
+ else
593
+ next
590
594
  end
591
595
  end
592
596
 
@@ -594,7 +598,7 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
594
598
  secgroup_ips[instance_details[:role]].flatten!
595
599
 
596
600
  # delete any nil's that we collected due to skipping private ips
597
- secgroup_ips[instance_details[:role]].delete_if {|x| x.nil? }
601
+ secgroup_ips[instance_details[:role]].delete_if {|x| x.nil? }
598
602
  end
599
603
 
600
604
  # dump the json to a temp file
@@ -618,20 +622,20 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
618
622
  # if we're passed a role, only deploy this role.
619
623
  def Stack.deploy_all(config, role_to_deploy = nil)
620
624
  Stack.validate(config)
621
-
625
+
622
626
  # this also populates out unspecified defaults, like az
623
627
  node_details = Stack.populate_config(config)
624
628
  # get info about all instances running in our account & AZs
625
629
  servers = Stack.get_all_instances(config)
626
630
 
627
631
  # this is our main loop iterator, generates each host
628
- config[:roles].each do |role,role_details|
632
+ config[:roles].each do |role,role_details|
629
633
  Logger.debug { "Iterating over roles, this is #{role}, role_details = #{role_details}" }
630
634
 
631
635
  (1..role_details[:count]).each do |p|
632
636
  hostname = Stack.generate_hostname(config, role, p)
633
637
  Logger.debug { "Iterating over nodes in #{role}, this is #{hostname}" }
634
-
638
+
635
639
  # configure the global :chef_server details if this the chef server
636
640
  if role_details[:chef_server]
637
641
  Stack.set_chef_server(config, hostname)
@@ -642,7 +646,7 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
642
646
  Logger.info { "#{hostname} already exists, skipping.." }
643
647
  next
644
648
  end
645
-
649
+
646
650
  Logger.debug { "Deploying #{role}, role_to_deploy = #{role_to_deploy}" }
647
651
  if ((role_to_deploy.nil?) || (role_to_deploy.to_s == role.to_s))
648
652
  if (role_details[:skip_chef_prereg] == true || role_details[:chef_server])
@@ -654,9 +658,9 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
654
658
  knife_client_list.sub!(/\s/,'')
655
659
  if knife_client_list.length() > 0
656
660
  # we should delete the client to make way for this new machine
657
- Logger.info `knife client delete --yes #{hostname}`
661
+ Logger.info `knife client delete --yes #{hostname}`
658
662
  end
659
-
663
+
660
664
  # knife node create -d --environment $CHEF_ENVIRONMENT $SERVER_NAME
661
665
  # knife node run_list add -d --environment $CHEF_ENVIRONMENT $SERVER_NAME "role[${ROLE}]"
662
666
  # this relies on .chef matching the stacks config (TODO: poke the Chef API directly?)
@@ -680,40 +684,47 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
680
684
  multipart_cmd = "#{libdir}/write-mime-multipart #{role_details[:bootstrap]} #{role_details[:cloud_config_yaml]}"
681
685
  Logger.debug { "multipart_cmd = #{multipart_cmd}" }
682
686
  multipart = `#{multipart_cmd}`
687
+ Logger.debug { "multipart = #{multipart}" }
683
688
  # 2) replace the tokens (CHEF_SERVER, CHEF_ENVIRONMENT, SERVER_NAME, ROLE)
689
+ Logger.debug { "Replacing %HOSTNAME% with #{hostname} in multipart" }
684
690
  multipart.gsub!(%q!%HOSTNAME%!, hostname)
685
691
 
686
- Logger.info "Chef server is #{config[:chef_server_hostname]}, which is in #{config[:node_details][config[:chef_server_hostname]][:region]}"
687
- Logger.info "#{hostname}'s region is #{config[:node_details][hostname][:region]}"
688
- # if this host is in the same region/az, use the private URL, if not, use the public url
689
- if (config[:node_details][hostname][:region] == config[:node_details][config[:chef_server_hostname]][:region]) && !config[:chef_server_private].nil?
690
- multipart.gsub!(%q!%CHEF_SERVER%!, config[:chef_server_private])
691
- elsif !config[:chef_server_public].nil?
692
- multipart.gsub!(%q!%CHEF_SERVER%!, config[:chef_server_public])
692
+ if config[:chef_server_hostname].nil?
693
+ Logger.info "config[:chef_server_hostname] is nil, skipping chef server substitution"
693
694
  else
694
- Logger.warn { "Not setting the chef url for #{hostname} as neither chef_server_private or chef_server_public are valid yet" }
695
- end
696
- multipart.gsub!(%q!%CHEF_ENVIRONMENT%!, config[:chef_environment])
697
- if File.exists?(config[:chef_validation_pem])
698
- multipart.gsub!(%q!%CHEF_VALIDATION_PEM%!, File.read(config[:chef_validation_pem]))
699
- else
700
- Logger.warn "Skipping #{config[:chef_validation_pem]} substitution in user-data"
695
+ Logger.info "Chef server is #{config[:chef_server_hostname]}, which is in #{config[:node_details][config[:chef_server_hostname]][:region]}"
696
+ Logger.info "#{hostname}'s region is #{config[:node_details][hostname][:region]}"
697
+ # if this host is in the same region/az, use the private URL, if not, use the public url
698
+ if (config[:node_details][hostname][:region] == config[:node_details][config[:chef_server_hostname]][:region]) && !config[:chef_server_private].nil?
699
+ multipart.gsub!(%q!%CHEF_SERVER%!, config[:chef_server_private])
700
+ elsif !config[:chef_server_public].nil?
701
+ multipart.gsub!(%q!%CHEF_SERVER%!, config[:chef_server_public])
702
+ else
703
+ Logger.warn { "Not setting the chef url for #{hostname} as neither chef_server_private or chef_server_public are valid yet" }
704
+ end
705
+ multipart.gsub!(%q!%CHEF_ENVIRONMENT%!, config[:chef_environment])
706
+ if File.exists?(config[:chef_validation_pem])
707
+ multipart.gsub!(%q!%CHEF_VALIDATION_PEM%!, File.read(config[:chef_validation_pem]))
708
+ else
709
+ Logger.warn "Skipping #{config[:chef_validation_pem]} substitution in user-data"
710
+ end
701
711
  end
712
+
702
713
  multipart.gsub!(%q!%SERVER_NAME%!, hostname)
703
714
  multipart.gsub!(%q!%ROLE%!, role.to_s)
704
715
  multipart.gsub!(%q!%DATA_DIR%!, role_details[:data_dir])
705
716
 
706
717
  Logger.info "Creating #{hostname} in #{node_details[hostname][:az]} with role #{role}"
707
718
 
708
- # this will get put in /meta.js
719
+ # this will get put in /meta.js
709
720
  metadata = { 'region' => node_details[hostname][:az], 'chef_role' => role }
710
721
 
711
722
  os = Stack.connect(config, node_details[hostname][:az])
712
- newserver = os.create_server(:name => hostname,
713
- :imageRef => config[node_details[hostname][:az]]['image_id'],
723
+ newserver = os.create_server(:name => hostname,
724
+ :imageRef => config[node_details[hostname][:az]]['image_id'],
714
725
  :flavorRef => config['flavor_id'],
715
726
  :security_groups=>[role_details[:security_group]],
716
- :user_data => Base64.encode64(multipart),
727
+ :user_data => Base64.encode64(multipart),
717
728
  :metadata => metadata,
718
729
  :key_name => config[:key_pair])
719
730
 
@@ -742,9 +753,9 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
742
753
  Logger.info "Attaching #{floating_ip} to #{hostname}\n via 'nova add-floating-ip'"
743
754
  # nova --os-region-name $REGION add-floating-ip $SERVER_NAME $FLOATING_IP
744
755
  floating_ip_add = `nova --os-region-name #{node_details[hostname][:az]} add-floating-ip #{hostname} #{floating_ip}`
745
- Logger.info floating_ip_add
746
- end
747
-
756
+ Logger.info floating_ip_add
757
+ end
758
+
748
759
  # refresh the secgroups ASAP
749
760
  Stack.secgroup_sync(config)
750
761
 
@@ -754,7 +765,7 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
754
765
  # convert when we got passed to an absolute path
755
766
  post_install_script_abs = File.realpath(config[:stackhome] + '/' + role_details[:post_install_script])
756
767
  post_install_cwd_abs = File.realpath(config[:stackhome] + '/' + role_details[:post_install_cwd])
757
-
768
+
758
769
  # replace any tokens in the argument
759
770
  public_ip = Stack.get_public_ip(config, hostname)
760
771
  role_details[:post_install_args].sub!(%q!%PUBLIC_IP%!, public_ip)
@@ -766,7 +777,7 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
766
777
  else
767
778
  Logger.info "Skipped role #{role}"
768
779
  end
769
- end
780
+ end
770
781
  end
771
782
  end
772
783
 
@@ -775,7 +786,7 @@ cookbook_path [ '<%=config[:stackhome]%>/cookbooks' ]
775
786
  # 1) cwd
776
787
  # 2) stackhome
777
788
  # 3) gemhome/lib
778
- dirs = [ './' ]
789
+ dirs = [ './' ]
779
790
  dirs.push(config[:stackhome])
780
791
  dirs.push(@@gemhome + '/lib')
781
792
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stack-kicker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-22 00:00:00.000000000 Z
12
+ date: 2013-05-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rdoc
@@ -133,13 +133,19 @@ extra_rdoc_files: []
133
133
  files:
134
134
  - .gitignore
135
135
  - .rvmrc
136
+ - CHANGELOG.md
136
137
  - Gemfile
137
138
  - LICENSE.txt
138
139
  - README.md
139
140
  - README.rdoc
140
141
  - Rakefile
141
142
  - bin/stack-kicker
142
- - doc/examples/Stackfile
143
+ - doc/examples/.rvmrc
144
+ - doc/examples/Gemfile
145
+ - doc/examples/apache-via-chef-server/Stackfile
146
+ - doc/examples/apache-via-cloud-init/Stackfile
147
+ - doc/examples/apache-via-cloud-init/cloud-init.sh
148
+ - doc/examples/apache-via-cloud-init/cloud-init.yaml
143
149
  - features/stack-kicker.feature
144
150
  - features/step_definitions/stack-kicker_steps.rb
145
151
  - features/support/env.rb