Mischa Taylor's Coding Blog

On the edge of time and reason

Getting Started Writing Chef Cookbooks the Berkshelf Way, Part 2

Updated December 29, 2013

  • Bumped apache2 cookbook reference from 1.7.x to 1.8.x
  • Bumped database cookbook reference from 1.4.x to 1.6.x
  • Per Nicholas Johns removed php 5.5 deprecated mysql* functions_
  • Replaced symbol references with strings to match part 1 changes
  • Added Windows instructions

Updated September 1, 2013

  • Bumped apache2 cookbook reference from 1.6.x to 1.7.x
  • Bumped database cookbook reference from 1.3.x to 1.4.x

Updated August 7, 2013

  • Fixed error in Iteration #10 test per Jeff Thomas

Updated July 23rd, 2013

  • Referenced Sean OMeara’s & Charles Johnson’s latest myface example app

This is a second article in a series on writing Opscode Chef cookbooks the Berkshelf Way. Here’s a link to Part 1. The source code examples covered in this article can be found on Github: https://github.com/misheska/myface

In this installment, Part 2, we’re going to create a new database recipe. In Part1 MyFace is just a web application serving up a static page. Now we’re going to enhance MyFace so that it stores account information in a persistent MySQL database.

Thanks go out to the Opscode Advanced Chef Cookbook Authoring class and specifically Sean OMeara and Charles Johnson for the database and PHP code used in this article.

Iteration #7 - Install MySQL

Edit metadata.rb and add a reference to the mysql cookbook. Also bump the version to 2.0.0 because we know that there will be incompatible API changes, moving to MySQL, per Semantic Versioning:

myface/metadata.rb
1
2
3
4
5
6
7
8
9
10
name             'myface'
maintainer       'Mischa Taylor'
maintainer_email 'mischa@misheska.com'
license          'Apache 2.0'
description      'Installs/Configures myface'
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
version          '2.0.0'

depends 'apache2', '~> 1.8.0'
depends 'mysql', '~> 4.0.0'

Create a new recipe called recipes/database.rb which includes the MySQL cookbook’s server recipe (this is a similar abstraction to what you created in Part 1 with recipes/webserver.rb):

myface/recipes/database.rb
1
2
3
4
5
6
7
8
9
10
#
# Cookbook Name:: myface
# Recipe:: database
#
# Copyright (C) 2012 YOUR_NAME
# 
# All rights reserved - Do Not Redistribute
#

include_recipe 'mysql::server'

Wire the database recipe into the MyFace cookbook by adding an include_recipe reference to recipes/default.rb:

myface/recipes/default.rb
1
2
3
4
5
6
7
8
9
10
# Cookbook Name:: myface
# Recipe:: default
#
# Copyright (C) 2013 YOUR_NAME
#
# All rights reserved - Do Not Redistribute
#

include_recipe 'myface::database'
include_recipe 'myface::webserver'

Run vagrant provision to converge your changes.

$ vagrant provision
[Berkshelf] This version of the Berkshelf plugin has not been fully tested on this version of Vagrant.
[Berkshelf] You should check for a newer version of vagrant-berkshelf.
[Berkshelf] If you encounter any errors with this version, please report them at https://github.com/RiotGames/vagrant-berkshelf/issues
[Berkshelf] You can also join the discussion in #berkshelf on Freenode.
[Berkshelf] Updating Vagrant's berkshelf: '/Users/misheska/.berkshelf/default/vagrant/berkshelf-20131228-44581-4bhc9d-default'
[Berkshelf] Using myface (2.0.0)
[Berkshelf] Using apache2 (1.8.14)
[Berkshelf] Installing mysql (4.0.14) from site: 'http://cookbooks.opscode.com/api/v1/cookbooks'
[Berkshelf] Installing openssl (1.1.0) from site: 'http://cookbooks.opscode.com/api/v1/cookbooks'
[Berkshelf] Installing build-essential (1.4.2) from site: 'http://cookbooks.opscode.com/api/v1/cookbooks'
[default] Chef 11.8.2 Omnibus package is already installed.
[default] Running provisioner: chef_solo...
Generating chef JSON and uploading...
Running chef-solo...
[2013-12-28T23:45:15-08:00] INFO: Forking chef instance to converge...
[2013-12-28T23:45:15-08:00] INFO: *** Chef 11.8.2 ***
[2013-12-28T23:45:15-08:00] INFO: Chef-client pid: 28502
[2013-12-28T23:45:16-08:00] INFO: Setting the run_list to ["recipe[myface::default]"] from JSON
[2013-12-28T23:45:16-08:00] INFO: Run List is [recipe[myface::default]]
[2013-12-28T23:45:16-08:00] INFO: Run List expands to [myface::default]
[2013-12-28T23:45:16-08:00] INFO: Starting Chef Run for myface-berkshelf
[2013-12-28T23:45:16-08:00] INFO: Running start handlers
[2013-12-28T23:45:16-08:00] INFO: Start handlers complete.
[2013-12-28T23:45:16-08:00] WARN: Cloning resource attributes for directory[/var/lib/mysql] from prior resource (CHEF-3694)
[2013-12-28T23:45:16-08:00] WARN: Previous directory[/var/lib/mysql]: /tmp/vagrant-chef-1/chef-solo-1/cookbooks/mysql/recipes/_server_rhel.rb:11:in `block in from_file'
[2013-12-28T23:45:16-08:00] WARN: Current  directory[/var/lib/mysql]: /tmp/vagrant-chef-1/chef-solo-1/cookbooks/mysql/recipes/_server_rhel.rb:20:in `from_file'
[2013-12-28T23:45:16-08:00] WARN: Cloning resource attributes for service[apache2] from prior resource (CHEF-3694)
[2013-12-28T23:45:16-08:00] WARN: Previous service[apache2]: /tmp/vagrant-chef-1/chef-solo-1/cookbooks/apache2/recipes/default.rb:24:in `from_file'
[2013-12-28T23:45:16-08:00] WARN: Current  service[apache2]: /tmp/vagrant-chef-1/chef-solo-1/cookbooks/apache2/recipes/default.rb:210:in `from_file'
[2013-12-28T23:45:33-08:00] INFO: package[mysql-server] installing mysql-server-5.1.71-1.el6 from base repository
[2013-12-28T23:45:43-08:00] INFO: directory[/var/log/mysql] created directory /var/log/mysql
[2013-12-28T23:45:43-08:00] INFO: directory[/var/log/mysql] owner changed to 27
[2013-12-28T23:45:43-08:00] INFO: directory[/var/log/mysql] group changed to 27
[2013-12-28T23:45:43-08:00] INFO: directory[/var/log/mysql] mode changed to 755
[2013-12-28T23:45:43-08:00] INFO: directory[/etc/mysql/conf.d] created directory /etc/mysql/conf.d
[2013-12-28T23:45:43-08:00] INFO: directory[/etc/mysql/conf.d] owner changed to 27
[2013-12-28T23:45:43-08:00] INFO: directory[/etc/mysql/conf.d] group changed to 27
[2013-12-28T23:45:43-08:00] INFO: directory[/etc/mysql/conf.d] mode changed to 755
[2013-12-28T23:45:43-08:00] INFO: template[initial-my.cnf] backed up to /var/chef/backup/etc/my.cnf.chef-20131228234543.651750
[2013-12-28T23:45:43-08:00] INFO: template[initial-my.cnf] updated file contents /etc/my.cnf
[2013-12-28T23:45:43-08:00] INFO: template[initial-my.cnf] sending start action to service[mysql-start] (immediate)
[2013-12-28T23:45:45-08:00] INFO: service[mysql-start] started
[2013-12-28T23:45:45-08:00] INFO: execute[assign-root-password] ran successfully
[2013-12-28T23:45:45-08:00] INFO: template[/etc/mysql_grants.sql] created file /etc/mysql_grants.sql
[2013-12-28T23:45:45-08:00] INFO: template[/etc/mysql_grants.sql] updated file contents /etc/mysql_grants.sql
[2013-12-28T23:45:45-08:00] INFO: template[/etc/mysql_grants.sql] owner changed to 0
[2013-12-28T23:45:45-08:00] INFO: template[/etc/mysql_grants.sql] group changed to 0
[2013-12-28T23:45:45-08:00] INFO: template[/etc/mysql_grants.sql] mode changed to 600
[2013-12-28T23:45:45-08:00] INFO: template[/etc/mysql_grants.sql] sending run action to execute[install-grants] (immediate)
[2013-12-28T23:45:45-08:00] INFO: execute[install-grants] ran successfully
[2013-12-28T23:45:45-08:00] INFO: execute[install-grants] sending restart action to service[mysql] (immediate)
[2013-12-28T23:45:50-08:00] INFO: service[mysql] restarted
[2013-12-28T23:45:50-08:00] INFO: service[mysql] enabled
[2013-12-28T23:45:52-08:00] INFO: Chef Run complete in 35.813790999 seconds
[2013-12-28T23:45:52-08:00] INFO: Running report handlers
[2013-12-28T23:45:52-08:00] INFO: Report handlers complete
[2013-12-28T23:45:15-08:00] INFO: Forking chef instance to converge...

Testing Iteration #7

Verify that the mysqld service is running on your vagrant guest by running the following command on Mac OS X/Linux:

$ vagrant ssh -c "sudo /sbin/service mysqld status"
mysqld (pid  8525) is running...

And on Windows:

> vagrant ssh -c "sudo /sbin/service mysqld status" -- -n -T
mysqld (pid  8525) is running...

Also check that MySQL is enabled to start on boot on Mac OS X/Linux:

$ vagrant ssh -c "sudo /sbin/chkconfig --list | grep mysqld"
mysqld         	0:off	1:off	2:on	3:on	4:on	5:on	6:off

And on Windows:

> vagrant ssh -c "sudo /sbin/chkconfig --list | grep mysqld" -- -n -T
mysqld         	0:off	1:off	2:on	3:on	4:on	5:on	6:off

If the service is set to be activated at runlevels 3 and 5, then MySQL is enabled to run under full multi-user text mode and full multi-user graphical mode, which is exactly the desired behavior.

Iteration #8 - Create the MyFace Database

We’ve installed MySQL, but we don’t have a database yet. Now we’re going to create a database to store information about our users with another cookbook, the database cookbook.

Add the database cookbook as a dependency in the metadata.rb file:

myface/metadata.rb
1
2
3
4
5
6
7
8
9
10
11
name             'myface'
maintainer       'Mischa Taylor'
maintainer_email 'mischa@misheska.com'
license          'Apache 2.0'
description      'Installs/Configures myface'
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
version          '2.0.0'

depends 'apache2', '~> 1.8.0'
depends 'mysql', '~> 4.0.0'
depends 'database', '~> 1.6.0'

Berkshelf automatically populates MySQL passwords for you. They were configured in the Vagrantfile when you ran berks cookbook in Part 1:

...
config.vm.provision :chef_solo do |chef|
  chef.json = {
    :mysql => {
      :server_root_password => 'rootpass',
      :server_debian_password => 'debpass',
      :server_repl_password => "replpass'
    }
  }
  ...
end
...

You can reference these passwords as variables in your Chef recipes, which we will do when we add some data attributes. Add the following attributes to attributes/default.rb so it looks like so:

myface/attributes/default.rb
1
2
3
4
5
6
7
8
9
10
default['myface']['user'] = 'myface'
default['myface']['group'] = 'myface'
default['myface']['name'] = 'myface'
default['myface']['config'] = 'myface.conf'
default['myface']['document_root'] = '/srv/apache/myface'

default['myface']['database']['host'] = 'localhost'
default['myface']['database']['username'] = 'root'
default['myface']['database']['password'] = node['mysql']['server_root_password']
default['myface']['database']['dbname'] = 'myface'

Describe the database to be created for MyFace in recipes/database.rb:

myface/recipes/database.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#
# Cookbook Name:: myface
# Recipe:: database
#
# Copyright (C) 2012 YOUR_NAME
#
# All rights reserved - Do Not Redistribute
#

include_recipe 'mysql::server'
include_recipe 'database::mysql'

mysql_database node['myface']['database']['dbname'] do
  connection(
    :host => node['myface']['database']['host'],
    :username => node['myface']['database']['username'],
    :password => node['myface']['database']['password']
  )
  action :create
end

Converge the changes with vagrant provision:

$ vagrant provision
[Berkshelf] This version of the Berkshelf plugin has not been fully tested on this version of Vagrant.
[Berkshelf] You should check for a newer version of vagrant-berkshelf.
[Berkshelf] If you encounter any errors with this version, please report them at https://github.com/RiotGames/vagrant-berkshelf/issues
[Berkshelf] You can also join the discussion in #berkshelf on Freenode.
[Berkshelf] Updating Vagrant's berkshelf: '/Users/misheska/.berkshelf/default/vagrant/berkshelf-20131229-47158-1y8wp6-default'
[Berkshelf] Using myface (2.0.0)
[Berkshelf] Using apache2 (1.8.14)
[Berkshelf] Using mysql (4.0.14)
[Berkshelf] Using openssl (1.1.0)
[Berkshelf] Using build-essential (1.4.2)
[Berkshelf] Using database (1.6.0)
[Berkshelf] Using postgresql (3.3.4)
[Berkshelf] Using apt (2.3.4)
[Berkshelf] Using aws (1.0.0)
[Berkshelf] Using xfs (1.1.0)
[default] Chef 11.8.2 Omnibus package is already installed.
[default] Running provisioner: chef_solo...
Generating chef JSON and uploading...
Running chef-solo...
[2013-12-29T00:40:09-08:00] INFO: Forking chef instance to converge...
[2013-12-29T00:40:09-08:00] INFO: *** Chef 11.8.2 ***
[2013-12-29T00:40:09-08:00] INFO: Chef-client pid: 5081
[2013-12-29T00:40:10-08:00] INFO: Setting the run_list to ["recipe[myface::default]"] from JSON
[2013-12-29T00:40:10-08:00] INFO: Run List is [recipe[myface::default]]
[2013-12-29T00:40:10-08:00] INFO: Run List expands to [myface::default]
[2013-12-29T00:40:10-08:00] INFO: Starting Chef Run for myface-berkshelf
[2013-12-29T00:40:10-08:00] INFO: Running start handlers
[2013-12-29T00:40:10-08:00] INFO: Start handlers complete.
[2013-12-29T00:40:10-08:00] WARN: Cloning resource attributes for directory[/var/lib/mysql] from prior resource (CHEF-3694)
[2013-12-29T00:40:10-08:00] WARN: Previous directory[/var/lib/mysql]: /tmp/vagrant-chef-1/chef-solo-1/cookbooks/mysql/recipes/_server_rhel.rb:11:in `block in from_file'
[2013-12-29T00:40:10-08:00] WARN: Current  directory[/var/lib/mysql]: /tmp/vagrant-chef-1/chef-solo-1/cookbooks/mysql/recipes/_server_rhel.rb:20:in `from_file'
[2013-12-29T00:40:11-08:00] INFO: package[autoconf] installing autoconf-2.63-5.1.el6 from base repository
[2013-12-29T00:40:18-08:00] INFO: package[bison] installing bison-2.4.1-5.el6 from base repository
[2013-12-29T00:40:25-08:00] INFO: package[flex] installing flex-2.5.35-8.el6 from base repository
[2013-12-29T00:40:31-08:00] INFO: package[gcc-c++] installing gcc-c++-4.4.7-4.el6 from base repository
[2013-12-29T00:40:41-08:00] INFO: package[mysql-devel] installing mysql-devel-5.1.71-1.el6 from base repository
[2013-12-29T00:41:17-08:00] WARN: Cloning resource attributes for service[apache2] from prior resource (CHEF-3694)
[2013-12-29T00:41:17-08:00] WARN: Previous service[apache2]: /tmp/vagrant-chef-1/chef-solo-1/cookbooks/apache2/recipes/default.rb:24:in `from_file'
[2013-12-29T00:41:17-08:00] WARN: Current  service[apache2]: /tmp/vagrant-chef-1/chef-solo-1/cookbooks/apache2/recipes/default.rb:210:in `from_file'
[2013-12-29T00:41:19-08:00] INFO: Chef Run complete in 69.220896743 seconds
[2013-12-29T00:41:19-08:00] INFO: Running report handlers
[2013-12-29T00:41:19-08:00] INFO: Report handlers complete
[2013-12-29T00:40:09-08:00] INFO: Forking chef instance to converge...

Testing Iteration #8

Run mysqlshow on your vagrant guest to display database information, verifying that the myface database was created on Mac OS X/Linux:

$ vagrant ssh -c "mysqlshow -uroot -prootpass"
+--------------------+
|     Databases      |
+--------------------+
| information_schema |
| myface             |
| mysql              |
| test               |
+--------------------+

And on Windows:

> vagrant ssh -c "mysqlshow -uroot -prootpass" -- -n -T

Note that myface is listed as a database name - success!

Iteration #9 - Create a MySQL user

It’s a good idea to create a user in MySQL for each one of your applications that has the ability to only manipulate the application’s database and has no MySQL administrative privileges.

Add some attributes to attributes/default.rb for your app user:

default['myface']['database']['app']['username'] = 'myface_app'
default['myface']['database']['app']['password'] = 'supersecret'

attributes/default.rb should look like so:

myface/attributes/default.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
default['myface']['user'] = 'myface'
default['myface']['group'] = 'myface'
default['myface']['name'] = 'myface'
default['myface']['config'] = 'myface.conf'
default['myface']['document_root'] = '/srv/apache/myface'

default['myface']['database']['host'] = 'localhost'
default['myface']['database']['username'] = 'root'
default['myface']['database']['password'] = node['mysql']['server_root_password']
default['myface']['database']['dbname'] = 'myface'

default['myface']['database']['app']['username'] = 'myface_app'
default['myface']['database']['app']['password'] = 'supersecret'

Edit recipes/database.rb and describe the MySQL database user:

...
  )
  action :create
end

mysql_database_user node['myface']['database']['app']['username'] do
  connection(
    :host => node['myface']['database']['host'],
    :username => node['myface']['database']['username'],
    :password => node['myface']['database']['password']
  )
  password node['myface']['database']['app']['password']
  database_name node['myface']['database']['dbname']
  host node['myface']['database']['host']
  action [:create, :grant]
end

After editing recipes/database.rb should look like the following:

myface/recipes/database.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#
# Cookbook Name:: myface
# Recipe:: database
#
# Copyright (C) 2012 YOUR_NAME
#
# All rights reserved - Do Not Redistribute
#

include_recipe 'mysql::server'
include_recipe 'database::mysql'

mysql_database node['myface']['database']['dbname'] do
  connection(
    :host => node['myface']['database']['host'],
    :username => node['myface']['database']['username'],
    :password => node['myface']['database']['password']
  )
  action :create
end

mysql_database_user node['myface']['database']['app']['username'] do
  connection(
    :host => node['myface']['database']['host'],
    :username => node['myface']['database']['username'],
    :password => node['myface']['database']['password']
  )
  password node['myface']['database']['app']['password']
  database_name node['myface']['database']['dbname']
  host node['myface']['database']['host']
  action [:create, :grant]
end

Converge the node to apply the changes:

$ vagrant provision

Testing Iteration #9

Check to see if the myface-app user is enabled as a local user by running the following mysql command on Mac OS X/Linux:

$ vagrant ssh -c 'mysql -uroot -prootpass -e "select user,host from mysql.user;"'
+------------+------------------+
| user       | host             |
+------------+------------------+
| repl       | %                |
| root       | 127.0.0.1        |
|            | localhost        |
| myface_app | localhost        |
| root       | localhost        |
|            | myface-berkshelf |
+------------+------------------+
Connection to 127.0.0.1 closed.

And on Windows:

> vagrant ssh -c "mysql -uroot -prootpass -e 'select user,host from mysql.user;'" -- -n -T

As you can see above, the myface_app@localhost user exists, so our cookbook did what was expected.

Also check to see that the myface_app user only has rights on the myface databse on Mac OS X/Linux:

$ vagrant ssh -c 'mysql -uroot -prootpass -e "show grants for 'myface_app'@'localhost';"'
Grants for myface_app@localhost
GRANT USAGE ON *.* TO 'myface_app'@'localhost' IDENTIFIED BY PASSWORD '*90BA3AC0BFDE07AE334CA523CB27167AE33825B9'
GRANT ALL PRIVILEGES ON `myface`.* TO 'myface_app'@'localhost'

And on Windows:

> vagrant ssh -c "mysql -uroot -prootpass -e 'show grants for "myface_app"@"localhost";'" -- -n -T

Iteration #10 - Create a table for users

Let’s create a SQL script to create a table modeling MyFace users and populate it with some initial data. Create a file files/default/myface-create.sql with the following content:

myface/files/default/myface-create.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* A table for myface users */

CREATE TABLE users(
 id CHAR (32) NOT NULL,
 PRIMARY KEY(id),
 user_name VARCHAR(64),
 url VARCHAR(256),
 email VARCHAR(128),
 neck_beard INTEGER
);

/* Initial records */
INSERT INTO users ( id, user_name, url, email, neck_beard ) VALUES ( uuid(), 'jtimberman', 'http://jtimberman.housepub.org', 'joshua@opscode.com', 4 );
INSERT INTO users ( id, user_name, url, email, neck_beard ) VALUES ( uuid(), 'someara', 'http://blog.afistfulofservers.net/', 'someara@opscode.com', 5 );
INSERT INTO users ( id, user_name, url, email, neck_beard ) VALUES ( uuid(), 'jwinsor', 'http://vialstudios.com', 'jamie@vialstudios.com', 4 );
INSERT INTO users ( id, user_name, url, email, neck_beard ) VALUES ( uuid(), 'cjohnson', 'http://www.chipadeedoodah.com/', 'charles@opscode.com', 3 );
INSERT INTO users ( id, user_name, url, email, neck_beard ) VALUES ( uuid(), 'mbower', 'http://www.webbower.com/', 'matt@webbower.com', 4 );

Add an attribute for the temporary location used for the SQL script we just created:

default['myface']['database']['seed_file'] = '/tmp/myface-create.sql'

The resultant attributes/default.rb should resemble the following:

myface/attributes/default.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
default['myface']['user'] = 'myface'
default['myface']['group'] = 'myface'
default['myface']['name'] = 'myface'
default['myface']['config'] = 'myface.conf'
default['myface']['document_root'] = '/srv/apache/myface'

default['myface']['database']['host'] = 'localhost'
default['myface']['database']['username'] = 'root'
default['myface']['database']['password'] = node['mysql']['server_root_password']
default['myface']['database']['dbname'] = 'myface'

default['myface']['database']['app']['username'] = 'myface_app'
default['myface']['database']['app']['password'] = 'supersecret'

default['myface']['database']['seed_file'] = '/tmp/myface-create.sql'

Modify recipes/database.rb so that the cookbook transfers the SQL script to the guest node and so that the SQL script executes. As you learned in Part 1, recipes should be idempotent, so you will need to add a not_if statement which ensures that the command is only executed when necessary.

...
  host 'localhost'
  action [:create, :grant]
end

# Write schema seed file to filesystem
cookbook_file node['myface']['database']['seed_file'] do
  source 'myface-create.sql'
  owner 'root'
  group 'root'
  mode '0600'
end

# Seed database with test data
execute 'initialize myface database' do
  command "mysql -h #{node['myface']['database']['host']} -u #{node['myface']['database']['app']['username']} -p#{node['myface']['database']['app']['password']} -D #{node['myface']['database']['dbname']} < #{node['myface']['database']['seed_file']}"
  not_if  "mysql -h #{node['myface']['database']['host']} -u #{node['myface']['database']['app']['username']} -p#{node['myface']['database']['app']['password']} -D #{node['myface']['database']['dbname']} -e 'describe users;'"
end

Once you have made these changes, recipes/database.rb should look like so:

myface/recipes/database.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#
# Cookbook Name:: myface
# Recipe:: database
#
# Copyright (C) 2012 YOUR_NAME
#
# All rights reserved - Do Not Redistribute
#

include_recipe 'mysql::server'
include_recipe 'database::mysql'

mysql_database node['myface']['database']['dbname'] do
  connection(
    :host => node['myface']['database']['host'],
    :username => node['myface']['database']['username'],
    :password => node['myface']['database']['password']
  )
  action :create
end

mysql_database_user node['myface']['database']['app']['username'] do
  connection(
    :host => node['myface']['database']['host'],
    :username => node['myface']['database']['username'],
    :password => node['myface']['database']['password']
  )
  password node['myface']['database']['app']['password']
  database_name node['myface']['database']['dbname']
  host node['myface']['database']['host']
  action [:create, :grant]
end

# Write schema seed file to filesystem
cookbook_file node['myface']['database']['seed_file'] do
  source 'myface-create.sql'
  owner 'root'
  group 'root'
  mode '0600'
end

# Seed database with test data
execute 'initialize myface database' do
  command "mysql -h #{node['myface']['database']['host']} -u #{node['myface']['database']['app']['username']} -p#{node['myface']['database']['app']['password']} -D #{node['myface']['database']['dbname']} < #{node['myface']['database']['seed_file']}"
  not_if  "mysql -h #{node['myface']['database']['host']} -u #{node['myface']['database']['app']['username']} -p#{node['myface']['database']['app']['password']} -D #{node['myface']['database']['dbname']} -e 'describe users;'"
end

Run vagrant provision to converge your changes:

$ vagrant provision

Testing Iteration #10

Run the following mysql command to dump the contents of the users table on Mac OS X/Linux:

$ vagrant ssh -c 'mysql -hlocalhost -umyface_app -psupersecret -Dmyface -e "select id,user_name from users;"'
id                                  user_name
216e03c2-ffe4-11e2-b1ad-080027c8	jtimberman
216e0890-ffe4-11e2-b1ad-080027c8	someara
216e0bce-ffe4-11e2-b1ad-080027c8	jwinsor
216e0eda-ffe4-11e2-b1ad-080027c8	cjohnson
216e11e6-ffe4-11e2-b1ad-080027c8	mbower

And on Windows:

> vagrant ssh -c "mysql -hlocalhost -umyface_app -psupersecret -Dmyface -e 'select id,user_name from users;'" -- -n -T

The output should look similar to what you see above - the data from the INSERT INTO statemens in the SQL script.

Iteration #11 - Install PHP

Let’s add some PHP scripting sizzle to sell the steak of the database we just created. We’re going to install Apache 2 mod_php5 module and the php-mysql package to support our PHP script.

Edit recipes/webserver.rb and add the following:

...
include_recipe 'apache2'
include_recipe 'apache2::mod_php5'

package 'php-mysql' do
  action :install
  notifies :restart, 'service[apache2]'
end

This will use the apache2 cookbook’s mod_php5 module to install PHP5 and install the php-mysql support package. After editing, recipes/webserver.rb should look like this:

myface/recipes/webserver.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#
# Cookbook Name:: myface
# Recipe:: webserver
#
# Copyright (C) 2013 YOUR_NAME
#
# All rights reserved - Do Not Redistribute
#

group node['myface']['group']

user node['myface']['user'] do
  group node['myface']['group']
  system true
  shell '/bin/bash'
end

include_recipe 'apache2'
include_recipe 'apache2::mod_php5'

package 'php-mysql' do
  action :install
  notifies :restart, 'service[apache2]'
end

# disable default site
apache_site '000-default' do
  enable false
end

# create apache config
template "#{node['apache']['dir']}/sites-available/#{node['myface']['config']}" do
  source 'apache2.conf.erb'
  notifies :restart, 'service[apache2]'
end

# create document root
directory "#{node['myface']['document_root']}" do
  action :create
  recursive true
end

# write site
cookbook_file "#{node['myface']['document_root']}/index.html" do
  mode "0644"
end

# enable myface
apache_site "#{node['myface']['config']}" do
  enable true
end

Run vagrant provision to converge your changes:

$ vagrant provision

Test Iteration #11

Run the following command to verify that the php5_module was successfully installed on Mac OS X/Linux:

$ vagrant ssh -c "sudo /usr/sbin/httpd -M | grep php5"
httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1 for ServerName
[Wed Aug 07 21:42:03 2013] [warn] NameVirtualHost *:80 has no VirtualHosts
Syntax OK
 php5_module (shared)

And on Windows:

> vagrant ssh -c "sudo /usr/sbin/httpd -M | grep php5" -- -n -T

Iteration #12 - Add PHP Sizzle

It’s the last iteration, get ready to see the PHP sizzle! First modify templates/default/apache2.conf.erb as follows:

myface/templates/default/apache2.conf.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Managed by Chef for <%= node['hostname'] %>
<VirtualHost *:80>
        ServerAdmin <%= node['apache']['contact'] %>

        DocumentRoot <%= node['myface']['document_root'] %>
        <Directory />
                Options FollowSymLinks
                AllowOverride None
        </Directory>
        <Directory <%= node['myface']['document_root'] %>>
                Options Indexes FollowSymLinks MultiViews
                AllowOverride None
                Order allow,deny
                allow from all
        </Directory>

        ErrorLog <%= node['apache']['log_dir'] %>/error.log

        LogLevel warn

        CustomLog <%= node['apache']['log_dir'] %>/access.log combined
        ServerSignature Off
</VirtualHost>

Next delete files/default/index.html with the following command:

$ rm files/default/index.html

You’ll be replacing it with the following parametized PHP script as templates/default/index.php.erb:

myface/templates/default/index.php.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
<!--
Copyright 2013, Opscode, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

-->

<?php
$db_host = 'localhost';
$db_user = 'root';
$db_pwd = '<%= node['mysql']['server_root_password'] %>';

$database = 'myface';
$table = 'users';

// UTILITY FUNCTIONS
function create_gravatar_hash($email) {
    return md5( strtolower( trim( $email ) ) );
}

function gravatar_img($email=null, $name=null, $size=null) {
    if(!$email) {
        return '';
    }

    $url =  'http://www.gravatar.com/avatar/';
    $url .= create_gravatar_hash($email);
    if($size) {
        $url .= "?s={$size}";
    }

    return sprintf('<img src="%s" alt="%s" />', $url, $name ? $name : '');
}

function neckbeard($rating) {
    $ratings = array(
        'Puberty awaits!',
        'Peach fuzz',
        'Solid week&#39;s growth',
        'Lumberjacks would be proud',
        'Makes dwarves weep',
    );

    return $ratings[(int) $rating - 1];
}

function fetch($host, $dbname, $dbuser, $dbpass, $table) {
    try {
        $connection = new PDO("mysql:host={$host};dbname={$dbname}", $dbuser, $dbpass);
        $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        $rc = $connection->query("SELECT COUNT(*) FROM {$table}");
        $count = $rc->fetchColumn();

        $result = $connection->query("SELECT * FROM {$table}");

        return array($result, $count);
    } catch(Exception $e) {
        die($e->getMessage());
    }
}

list($result, $fields_num) = fetch($db_host, $database, $db_user, $db_pwd, $table);

?>
<!DOCTYPE html>
<html lang="en">
<head>
    <title>MyFace Users</title>
    <style>
    * {
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        box-sizing: border-box;
    }
    
    html, body {
        margin: 0;
        padding: 0;
    }
    
    html {
        background: #999;
    }
    
    body {
        max-width: 480px;
        margin: 0 auto;
        font-family: Arial, Helvetica, sans-serif;
        color: #222;
        padding: 20px;
        border: 1px solid #666;
        -webkit-box-shadow: 0 0 5px rgba(0,0,0,0.3);
        -moz-box-shadow: 0 0 5px rgba(0,0,0,0.3);
        box-shadow: 0 0 5px rgba(0,0,0,0.3);
        background: #FFF;
    }
        
    a:link {
        text-decoration: none;
        color: #777;
    }
    
    a:hover,
    a:focus {
        text-decoration: underline;
    }
    
    h1 {
        text-align: center;
        margin-top: 0;
    }
        
    h1 span {
        color: #00C;
    }
    
    h2 {
        font-size: 24px;
        line-height: 1.0;
        margin: 0 0 10px;
    }
    
    p {
        font-size: 14px;
        line-height: 18px;
        margin: 10px 0;
    }
    
    p:last-child {
        margin-bottom: 0;
    }
    
    .email {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    
    /* Adapted from OOCSS
        * mod object (https://github.com/stubbornella/oocss/blob/master/core/module/mod.css)
        * media object (https://github.com/stubbornella/oocss/blob/master/core/media/media.css)
    */
    article {
        display: block;
        overflow: hidden;
        margin-bottom: 20px;
        border: 1px solid #CCC;
        background: #EEE;
        -webkit-border-radius: 4px;
        -moz-border-radius: 4px;
        border-radius: 4px;
        -webkit-box-shadow: 1px 1px 1px rgba(0,0,0,0.3) inset;
        -moz-box-shadow: 1px 1px 1px rgba(0,0,0,0.3) inset;
        box-shadow: 1px 1px 1px rgba(0,0,0,0.3) inset;
    }

    article .img {
        float: left;
        margin-right: 10px;
    }

    article .img img {
        display: block;
    }

    article .imgExt {
        float: right;
        margin-left: 10px;
    }

    article .bd {
        overflow: hidden;
        padding: 10px 0;
    }
    </style>
</head>
<body>
    <h1>Welcome to My<span>Face</span>!</h1>

    <?php while($row = $result->fetch(PDO::FETCH_OBJ)): ?>
    <article>
        <a href="<?php echo $row->url ?>" class="img" target="_blank">
            <?php echo gravatar_img($row->email, $row->user_name, 150) ?>
        </a>
        <div class="bd">
            <h2><?php echo $row->user_name ?></h2>
            <p><a href="<?php echo $row->url ?>" target="_blank" class="email"><?php echo $row->url ?></a></p>
            <p>Neckbeard rating: <?php echo neckbeard($row->neck_beard) ?></p>
        </div>
    </article>
    <?php endwhile; ?>
</body>
</html>
<?php mysql_free_result($result); ?>

Finally modify recipes/webserver.rb to use index.php.erb template to generate a new index.php document root:

myface/recipes/webserver.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#
# Cookbook Name:: myface
# Recipe:: webserver
#
# Copyright (C) 2013 YOUR_NAME
#
# All rights reserved - Do Not Redistribute
#

group node['myface']['group']

user node['myface']['user'] do
  group node['myface']['group']
  system true
  shell '/bin/bash'
end

include_recipe 'apache2'
include_recipe 'apache2::mod_php5'

package 'php-mysql' do
  action :install
  notifies :restart, 'service[apache2]'
end

# disable default site
apache_site '000-default' do
  enable false
end

# create apache config
template "#{node['apache']['dir']}/sites-available/#{node['myface']['config']}" do
  source 'apache2.conf.erb'
  notifies :restart, 'service[apache2]'
end

# create document root
directory "#{node['myface']['document_root']}" do
  action :create
  mode '0755'
  recursive true
end

# write site
template "#{node['myface']['document_root']}/index.php" do
  source 'index.php.erb'
  mode '0644'
end

# enable myface
apache_site "#{node['myface']['config']}" do
  enable true
end

Since we changed the document root and our recipe contains no statements to remove the old index.html document root, we’ll need to destroy our vagrant test node and do a full vagrant up again, otherwise if we visit http://33.33.33.10 again, we’ll just see the old document root:

$ vagrant destroy -f
$ vagrant up

Testing Iteration #12

Visit http://33.33.33.10 Now you should see the lovely new PHP version of Myface.

myfacephp

More to Come!

In Part 3, we’ll introduce a new tool test-kitchen and show you how to automate all the tests you’ve been doing manually to test each iteration.

If you want to see the full source for MyFace, check out the following GitHub link: https://github.com/misheska/myface