The best - and sometimes the worst - thing about Chef is its thriving
ecosystem. There appear to be cookbooks for just about everything,
including one just for installing emacs. And there are lots of tools,
lots of instructions, and blog posts about using the tools. So many in
fact that it is sometimes hard to sort out what is what - especially
as many of them are focused on showing you an example project rather
than talking about broad concepts. I have picked up a number of useful
tidbits but it’s been a little tough to integrate everything.
So, let me start with what I think I want. I want to use chef to set
up my Linode VPS. Before I make changes to the server, I would like to
run the same recipes locally on a Vagrant VM and run some integration
tests to check that I am going to get what I intend. Eventually I may
want to add unit tests for my custom cookbooks but for now, I want to
just focus on ‘outside in’ testing which, in this situation, seems to
me to be integration tests. This blog post,
Getting Started Writing Chef Cookbooks the Berkshelf Way, Part 3,
appears to do what I want to do, so let’s give that a try.
Initialize
I was originally going to manage everything using chef solo because I
thought that would be easier. But it turns out that chef and all the
tooling around it are optimized for using chef server to manage lots
of nodes, so chef server is actually the easier option - especially
since Opscode has a free tier for managing up to five nodes.
When I created my Linode VM with knife-linode
I did it all on the command line - no files. So that means that while
I have a node named ‘linode’ on my hosted chef and my Linode VPS is
set up as a client of that node, I don’t have any files and no git
repository keeping track of anything. So let’s see if I can construct
something to connect to that node on https://manage.opscode.com/.
So I stared with the chef gemset I had previously set up and upgraded
(chef 11.10.0, kitchen-vagrant 0.14.0, test-kitchen 1.1.1).
kitchen init created the .kitchen.yml file which is used to
configure my test kithcet setup. I added the port forwarding I will
need once I have a web server running on my VM. It is configured to
spin up a Vagrant VM with the Opscode Ubuntu 12.04 provisionless VM
and then use chef_omnibus to install the latest version of chef and
provision with chef_solo. I am not sure whether I should be using
chef_solo (so I can test out my cookbooks before they get uploaded to
Opscode) or if I should change that configuration to point to the
linode node on my chef server.
First server spec
So based on the example I worked through from the TDD book, and some
other blog posts,
I created a spec file in
chef-linode/test/integration/default/serverspec/emacs_spec.rb
require'spec_helper'describe"emacs cookbook"doit"should have installed emacs"doexpect(package'emacs23-nox').tobe_installedendend
I think this should indicate to test kitchen that I want it to use
serverspec to check that I have installed emacs on the VM. Right now,
I don’t have a spec_helper file; according to the Getting Started
section, I get one by doing serverspec-init. But I am a little
unclear about where to put it. The serverspec docs
indicate I should have the directory a directory structure like:
|--spec||--spec_helper.rb||--<servername># e.g localhost or cynthiakiser.com||--emacs_spec.rb
But the blog post
I am following, shows a directory structure of:
|--serverspec||--spec_helper.rb||--<servername># e.g localhost or cynthiakiser.com||--emacs_spec.rb
And I need a spec_helper.rb. serverspec-init will generate one:
I am going to try a combination. I moved my emacs_spec.rb into
spec/localhost/ and then renamed the spec directory to
serverspec. I left the includes of the SpecInfra::Helper modules but
removed the sudo stuff and replaced it with the check_os stuff from
the blog post. So now I have:
If you are using code to set up your infrastructure, then that code
demands as good or better software development practices as your
application code. One of the reasons I wanted to try Chef is because I
knew that people were doing automated testing of chef
configurations. (The puppet community may be doing similar kinds of
testing; I don’t know.) There is even a book about TDD and Chef:
Test-Driven Infrastructure With Chef
(and it’s even in its second edition). So I am going to try working
through it. The first several chapters are background or things I have
sort of done already. But they start to use Opscode’s Hosted Chef and
Vagrant in chapter 4.
Setting Up a New Playground
When I did the Quick Start exercises, I downloaded the starter kit
which included a .chef directory with a knife configuration file and
two .pem files: ckiser.pem and ckiser-validator.pem. I explored
downloading the starter kit again - but it wanted to reset my keys
which would have made my example stuff not work any more. It took me a
little while to figure out the instrutions on
http://docs.opscode.com/config_rb_knife.html
mean that one or the other file is read - not that one is read and
then the second file is read, overwriting configuration variables. I
was hoping to put shared information in ~/.chef/knife.rb and then
project specific information in a knife.rb in the project
directory. From my ‘puts’ statement, only one of the files is read - either
the one in the current directory or the one in ~/.chef/.
I can still get part of what I want - reuse of my keys - but moving
them to ~/.chef/ and then editing my knife.rb file to look for them
there. And then I created a new directory for working the TDI Chef
exercises, ~/chef-tdd/. I copied the knife.rb from the starter kit
into ~/.chef/knife.rb. I also created a cookbooks directory and copied
the chefignore file from the starter kit in there. And added the
extensive .gitignore from the starter kit. There are some other things
I think I will probably need but am going to wait until something I am
doing uses them before mving them here.
Vagrant
The last part of chapter 4 is about installing Vagrant and using it to
create virtual machines. Mostly this is just like what I have been
doing for a while now - but it linked me to a very promising source
for base boxes, Opscode’s Bento Boxes
One thing that makes them especially attractive is that the don’t come
with Chef preinstalled. Instead they recommend installing the most
current Chef using the vagrant-omnibus plugin. From the TDI book:
The plug-in we installed into Vagrant (vagrant plugin install
vagrant-omnibus) works with Vagrant boxes that do not have Chef
installed, and adds a hook to vagrant up to install Chef using the
omnibus package, just as we did in “Exercise 1: Install Chef ” on
page 47. This helps keep the Vagrant box slim and as close to
upstream as possible, and does not require a fleet of Vagrant boxes
to be created with every Chef patch release.
I want to test with a box that matches my Linode VPS as closely as
possible so despite the fact I already have an Opscode Ubuntu 12.04 in
32 bit, I downloaded and added the 64 bit version:
Then I did vagrant up. But I didn’t see the lines I expected
regarding installing chef. I tried destroying the VM and then tring
vagrant up again - same result. I do have warnings about the Guest
Additions not matching the version of VirtualBox that the Opscode base
boxes were built against:
I currently have Vagrant 1.2.2 installed. Looking at vagrantup.com, it
looks like the latest is 1.4.3. Let’s see if upgrading Vagrant fixes
this. I downloaded the latest .dmg file and ran the installer. Then
my next attempt at vagrant up gave me:
I unstalled both plugins. I don’t remember installing
vagrant-berkshelf so, for now, I am not going to reinstall it. I only
reinstalled vagrant-omnibus (1.2.1). But still no joy: no chef-client
or knife when I log into the box. ARRRGGGGGG because I was editing the
wrong Vagrantfile! I had put the line about vagrant-omnibus into the
file in chef-repo that I had opened for reference. OK let’s destroy
and recreate the VM. And now, we get chef:
04:20PM](brazen:~/chef-tdd) $ vagrant up
Bringingmachine'default'upwith'virtualbox'provider...[default]Importingbasebox'opscode-ubuntu-12.04'...[default]MatchingMACaddressforNATnetworking...[default]SettingthenameoftheVM...[default]Clearinganypreviouslysetforwardedports...[default]Clearinganypreviouslysetnetworkinterfaces...[default]Preparingnetworkinterfacesbasedonconfiguration...[default]Forwardingports...[default]--22=>2222(adapter1)[default]--80=>8080(adapter1)[default]BootingVM...[default]Waitingformachinetoboot.Thismaytakeafewminutes...[default]Machinebootedandready![default]Settinghostname...[default]Mountingsharedfolders...[default]--/vagrant
[default]InstallingChef11.8.2Omnibuspackage...[default]DownloadingChef11.8.2forubuntu...[default]downloadinghttps://www.opscode.com/chef/metadata?v=11.8.2&prerelease=false&p=ubuntu&pv=12.04&m=x86_64[default]tofile/tmp/install.sh.1143/metadata.txt[default]tryingwget...[default]urlhttps://opscode-omnibus-packages.s3.amazonaws.com/ubuntu/12.04/x86_64/chef_11.8.2-1.ubuntu.12.04_amd64.debmd53d3b3662830a44eeec71aadc098a4018sha256a5b00a24e68e29a01c7ab9de5cdaf0cc9fd1c889599ad9af70293e5b4de8615c[default]downloadedmetadatafilelooksvalid...[default]downloadinghttps://opscode-omnibus-packages.s3.amazonaws.com/ubuntu/12.04/x86_64/chef_11.8.2-1.ubuntu.12.04.amd64.deb[default]tofile/tmp/install.sh.1143/chef_11.8.2_amd64.deb[default]tryingwget...[default]Checksumcomparewithsha256sumsucceeded.[default]InstallingChef11.8.2[default]installingwithdpkg...[default]Selectingpreviouslyunselectedpackagechef.[default](Readingdatabase...[default]54659filesanddirectoriescurrentlyinstalled.)[default]Unpackingchef(from.../chef_11.8.2_amd64.deb)...[default]Settingupchef(11.8.2-1.ubuntu.12.04)...[default]ThankyouforinstallingChef $ vagrant ssh
WelcometoUbuntu12.04.3LTS(GNU/Linux3.8.0-29-genericx86_64)*Documentation:https://help.ubuntu.com/Lastlogin:TueNov2611:27:552013from10.0.2.2vagrant@vagrant-ubuntu-12-04:~$chef-client--versionChef:11.8.2
Ch 7.2: Berkshelf
Much of configuration management (and really any software development)
is dependency management. Throughout the TDI book we were managing
that manually - downloading cookbooks, checking dependencies,
downloading more cookbooks. Finally in chapter 7 the author introduces
Berkshelf to do the managing for us. So now I am going to need the
vagrant-berkshelf plugin that I delayed reinstalling in the section
above. vagrant plugin install vagrant-berkshelf. And then the book
told me to run berks configure. This creates a default configuration
file in ~/.berkshelf/config.json. Not really sure what the implications
of some of the items in this are. I’ll have to see as they arise.
Since I didn’t make the stand alone IRC cookbook from chapter 3, I
can’t do the berks init stuff. And the discussion of Berkshelf kind
of trailed off promising more information about preferred workflows
later in the chapter.
Ch 7.3: Application Cookbooks and Test Kitchen
What are application cookbooks and why should we use them?
The application cookbook pattern is characterized by having decided
the top-level service that we provide and creating a cookbook for
that service. That cookbook wraps all the dependent services that
are needed to deliver the top-level service. … This looks a lot
like the kind of thing that might be accomplished using a Chef role,
but has some significant advantages. First of all, cookbooks can be
explicitly versioned and tracked in a way that roles can’t.
If there is a need to alter the behavior of an upstream cookbook,
attributes can be set in a recipe, and if functionality needs to be
added, tested, or tweaked, this can be achieved by wrapping upstream
cookbooks in a manner that looks much like object inheritance. This
has the twin advantages again of being testable, but also of
avoiding constant fork‐ ing of upstream cookbooks.
The test harness tool of choice - at least of this author - is
Test Kitchen. I installed
it with gem install test-kitchen. The tests themselves are created
using a combination of cucumber, rspec, and leibniz (a library written
by the author).
Things were going OK until about page 200 where there were some
missing steps - such as where are we putting our specs and how did we
get our spec_helper - and for that matter, where did we get rspec
since it wasn’t on the list of gems we were told to add to our
Gemfile. The errata page has a complaint from someone else who noticed
this - but not resolution. I spent some time wandering around, read,
asked some questions on the #chef IRC channel. And then with some
perspective, went back to the book to see if I could reverse engineer
stuff using the code listings.
First, if we are going to run rspec -fd, we need rspec installed. So
I added it to the Gemfile and did bundle install. Now I can run
rspec --init and that created a spec directory and put a
spec_helper.rb file in there. I am kind of unclear about where to put
the integration test at the bottom of p 202 so I skipped them and
started with the unit test at the top of p 204. I typed in the ‘let’
and the first ‘it’ and then ran the spec. It complained uninitialized
constant ChefSpec so I added ‘chefspec’ to my Gemfile and a “require
‘chefspec’” to the top of my default_spec.rb. Then it complained it
couldn’t find chef. I know I have chef - but it wasn’t in my
Gemfile. So I added it to my Gemfile and reran ‘bundle’. Now I am
getting an odd error about about Chef - and a really old version of
chef. How did that get in here? And how did I end up with chefspec
0.0.1?
I created a completely new rvm gemset, removed my Gemfile.lock and did
a completely new bundle install. This time I only have chef 11.8.2
along with chef-spec 3.2.0 But even after fiddling with how I required
chefspec, I am still getting complaints abotu uninitialized constant
ChefSpec::ChefRunner. So I am going to give up on the example in
Chapter 7 “Acceptance Testing: Cucumber and Liebniz”.
Ch 7.5: Integration Testing: Test Kitchen with Serverspec
The next section on integration testing does a better job of showing
us the steps. But introduces one more layer - using Test Kitchen to
manage your Vagrant boxes. It is sort of nice because it gives you one
file that declares which OSs you are going to test your cookbook on -
and takes care of setting up what one needs to do that. But it is one
additional layer AND it means I am swimming in Vagrantfiles - most of
which are not used. I had thought I would test my full configuration
on a local Vagrant instance, so I created a Vagrantfile at the top of
my chef-tdd directory. When I did berks cookbook cnk-blog, that
automatically created a Vagrant file. And now when I edited the
.kitchen.ym file (which berks also created for me) and ran kitchen
create all, that created a Vagrantfile in
.kitchen/kitchen-vagrant/default-ubuntu-1204/ I think it is this last
Vagrantfile that I am actually seeing when I do kitchen list - since
I can’t ssh in when I am in ~/chef-tdd/cookbooks/cnk-blog/ but I can
if I am in
~/chef-tdd/cookbooks/cnk-blog/.kitchen/kitchen-vagrant/default-ubuntu-1204
My cookbook doesn’t yet have a run list, but if I run kitchen
converge anyway, that installs ruby and chef from the Opscode Omnibus
installer. So now, where do I put the tests. I don’t think I want to
use BATS unless I have to - I am better at Ruby than bash. So I read
that part but then started working through the Serverspec exercises
starting at p 241. I created a test file in the magic place (test
kitchen figures out it needs busser and then what kinds of tests to
run based on the directory names).
$mkdir-ptest/integration/default/serverspec/localhost# create file cnk-blog_spec.rb$cattest/integration/default/serverspec/localhost/cnk-blog_spec.rbrequire'spec_helper'describe"Cynthia's static html blog site"doit'shouldhaveinstalledapache'doexpect(packageapache2).tobe_installedendend$kitchenverify----->StartingKitchen(v1.1.1)----->Settingup<default-ubuntu-1204>...Fetching:thor-0.18.1.gem(100%)Fetching:busser-0.6.0.gem(100%)Successfullyinstalledthor-0.18.1Successfullyinstalledbusser-0.6.02gemsinstalled----->SettingupBusserCreatingBUSSER_ROOTin/tmp/busserCreatingbusserbinstubPluginserverspecinstalled(version0.2.6)----->RunningpostinstallforserverspecpluginFinishedsettingup<default-ubuntu-1204>(0m22.57s).----->Verifying<default-ubuntu-1204>...Suitepathdirectory/tmp/busser/suitesdoesnotexist,skipping.Uploading/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb(mode=0664)----->Runningserverspectestsuite/opt/chef/embedded/bin/ruby-I/tmp/busser/suites/serverspec-S/opt/chef/embedded/bin/rspec/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb--color--formatdocumentation/opt/chef/embedded/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in`require':cannotloadsuchfile--spec_helper(LoadError)from/opt/chef/embedded/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in`require'from/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb:1:in`<top(required)>'from/tmp/busser/gems/gems/rspec-core-2.14.7/lib/rspec/core/configuration.rb:896:in`load'from/tmp/busser/gems/gems/rspec-core-2.14.7/lib/rspec/core/configuration.rb:896:in`blockinload_spec_files'from/tmp/busser/gems/gems/rspec-core-2.14.7/lib/rspec/core/configuration.rb:896:in`each'from/tmp/busser/gems/gems/rspec-core-2.14.7/lib/rspec/core/configuration.rb:896:in`load_spec_files'from/tmp/busser/gems/gems/rspec-core-2.14.7/lib/rspec/core/command_line.rb:22:in`run'from/tmp/busser/gems/gems/rspec-core-2.14.7/lib/rspec/core/runner.rb:80:in`run'from/tmp/busser/gems/gems/rspec-core-2.14.7/lib/rspec/core/runner.rb:17:in`blockinautorun'/opt/chef/embedded/bin/ruby-I/tmp/busser/suites/serverspec-S/opt/chef/embedded/bin/rspec/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb--color--formatdocumentationfailedRubyScript[/tmp/busser/gems/gems/busser-serverspec-0.2.6/lib/busser/runner_plugin/../serverspec/runner.rb/tmp/busser/suites/serverspec]exitcodewas1>>>>>>Verifyfailedoninstance<default-ubuntu-1204>.>>>>>>Pleasesee.kitchen/logs/default-ubuntu-1204.logformoredetails>>>>>>------Exception------->>>>>>Class:Kitchen::ActionFailed>>>>>>Message:SSHexited(1)forcommand:[sh-c'BUSSER_ROOT="/tmp/busser"GEM_HOME="/tmp/busser/gems"GEM_PATH="/tmp/busser/gems"GEM_CACHE="/tmp/busser/gems/cache";exportBUSSER_ROOTGEM_HOMEGEM_PATHGEM_CACHE;sudo-E/tmp/busser/bin/bussertest']>>>>>>----------------------
Oops. I didn’t read carefully and missed creating the helper file in
cnk-blog/test/integration/default/serverspec/spec_helper.rb
Now I get properly failing tests - that is failing because I have not
written the cookbook code to make them work.
$kitchenverify----->StartingKitchen(v1.1.1)----->Verifying<default-ubuntu-1204>...Removing/tmp/busser/suites/serverspecUploading/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb(mode=0664)Uploading/tmp/busser/suites/serverspec/spec_helper.rb(mode=0664)----->Runningserverspectestsuite/opt/chef/embedded/bin/ruby-I/tmp/busser/suites/serverspec-S/opt/chef/embedded/bin/rspec/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb--color--formatdocumentationCynthia'sstatichtmlblogsiteNopackagesfoundmatchingapache2.shouldhaveinstalledapache(FAILED-1)haveenabledtheapacheservice(FAILED-2)httpd:unrecognizedserviceberunningtheapacheservice(FAILED-3)shouldlistenonport80(FAILED-4)shouldhaveavirtualhostforcnk-blogFailures:1)Cynthia'sstatichtmlblogsiteshouldhaveinstalledapacheFailure/Error:expect(package'apache2').tobe_installeddpkg-query-f'${Status}'-Wapache2|grep'^installokinstalled$'expectedPackage"apache2"tobeinstalled# /tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb:5:in `block (2 levels) in <top (required)>'2)Cynthia'sstatichtmlblogsitehaveenabledtheapacheserviceFailure/Error:expect(service'httpd').tobe_enabledls/etc/rc3.d/|grep--'^S..httpd'||grep'starton'/etc/init/httpd.confgrep:/etc/init/httpd.conf:NosuchfileordirectoryexpectedService"httpd"tobeenabled# /tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb:9:in `block (2 levels) in <top (required)>'3)Cynthia'sstatichtmlblogsiteberunningtheapacheserviceFailure/Error:expect(service'httpd').tobe_runningpsaux|grep-w--httpd|grep-qvgrepexpectedService"httpd"toberunning# /tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb:13:in `block (2 levels) in <top (required)>'4)Cynthia'sstatichtmlblogsiteshouldlistenonport80Failure/Error:expect(port80).tobe_listeningnetstat-tunl|grep--:80\expectedPort"80"tobelistening# /tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb:17:in `block (2 levels) in <top (required)>'Finishedin0.06817seconds5examples,4failuresFailedexamples:rspec/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb:4# Cynthia's static html blog site should have installed apacherspec/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb:8# Cynthia's static html blog site have enabled the apache servicerspec/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb:12# Cynthia's static html blog site be running the apache servicerspec/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb:16# Cynthia's static html blog site should listen on port 80/opt/chef/embedded/bin/ruby-I/tmp/busser/suites/serverspec-S/opt/chef/embedded/bin/rspec/tmp/busser/suites/serverspec/localhost/cnk-blog_spec.rb--color--formatdocumentationfailedRubyScript[/tmp/busser/gems/gems/busser-serverspec-0.2.6/lib/busser/runner_plugin/../serverspec/runner.rb/tmp/busser/suites/serverspec]exitcodewas1>>>>>>Verifyfailedoninstance<default-ubuntu-1204>.>>>>>>Pleasesee.kitchen/logs/default-ubuntu-1204.logformoredetails>>>>>>------Exception------->>>>>>Class:Kitchen::ActionFailed>>>>>>Message:SSHexited(1)forcommand:[sh-c'BUSSER_ROOT="/tmp/busser"GEM_HOME="/tmp/busser/gems"GEM_PATH="/tmp/busser/gems"GEM_CACHE="/tmp/busser/gems/cache";exportBUSSER_ROOTGEM_HOMEGEM_PATHGEM_CACHE;sudo-E/tmp/busser/bin/bussertest']>>>>>>----------------------
OpsCode’s VirtualBox VM seems pretty light, sudo du -sh / gives
1.1G. Can I use that image to install stuff on Linode? Or am I better
off starting with Linode’s Ubuntu 12.04 LTS image? I think I probably
want to start with Linode’s image. While looking for other stuff, I
ran across the knife-linode
plugin. Looking at the docs on the Opscode site,
there seem to be configuration parameters for knife-linode that will
let you create a server using one of Linode’s stock images so let’s
see if I can use that to create a box.
geminstallknife-linode# Got version 0.2.0
To play with it, I needed to go to the Linode “My Profile”
page and create an API key. Then I can list my current servers:
The user doesn’t seem to have been created. So I can’t ssh in as cnk
(and cnk isn’t in /etc/passwd), but I can ssh in as root. And I don’t
seem to have any ruby or chef - perhaps because I didn’t choose a
distro or template file. Let’s try that again - with --distro
chef-full added.
That give similar output (on a new IP). But when I logged in, I still
don’t have chef or ruby. In fact, there isn’t anything in /opt at
all. Let’s see if I can bootstrap chef by hand.
That seems a bit better; now we have ruby 1.9.3-p484 in
/opt/chef/embedded/bin/ruby. And it came with rubygems 1.8.24, bundler
(1.1.5), rake (10.1.0, 0.9.2.2), and chef (11.8.2). There are a couple
of gems that have more recent versions but I decided not to try
running ‘gem update’ because the installed versions may have been
chosen specifically to satisfy chef dependencies.
I’ll probably want to have chef doing my updates but for now, I think
I should have the server patched so ran apt-get update and apt-get
upgrade by hand for now. (Before I ran the upgrade, I took a list of
the packages that were installed originally with dpkg-query -W >
/root/original-packages.txt so I would know what the Linode Precise
Penguin image came with.)
Connecting my Linode node to Chef server
After installing chef with the knife bootstrap command, I see it tries
to converge the new node. However, there is nothing in my run list -
and no cookbook repository for this new server. Time to make one - and
then put my Linode API configuration into the knife.rb file so I can
quit passing it on the command line all the time.
UGH actually knife seems to have created a node on the server for
me. Was that during knife linode server create? or during the knife
bootstrap command? I can’t really tell from the docs. I should have
passed -N <name> to knife linode server create so that my node
would not end up named ‘localhost’ (and then when that didn’t do the
bootstrapping, again send the name). From what I can tell by Googling,
you can’t really rename a node, though there are delete and readd
proceedures that end up being quite similar. I think I’ll try this one
to get the chef node for my Linode sever named something more sensible
than localhost. I skipped step 6 since I didn’t see ‘localhost’ in
the /etc/chef/client.rb file. Running chef-client -N linode gave:
My group at $WORK has decided to start using Django as our web
development framework so I am working my way through
the tutorial.
First off, yeah! there is actually a tutorial! Nice to be using a
mature technology with good documentation.
The overview
does a nice job of showing off some basics - models (including
relationships), routing/urls, views, and templates (including template
composition and reuse). Which brings me to my first mental
mapping. One basic design pattern for web apps is known as MVC - Model
View Controller. So Rails calls three of the directories under ‘app’
models, views, and controllers. There is a fourth directory named
helpers, which is an awkward catch-all that tries to take the logic
out of the view layer, with variable success. And the urls or routes
are not defined within ‘app’ at all and instead are defined in the
config directory in ‘config/routes.rb’. Rails reuses the names from
MVC but in so doing sometimes ends up with some odd compromises. The
Django breakdown, on the other hand, seems to do a nicer job of
splitting the controller and view logic into the logic part (named
views) and the display part, in templates.
One of the best parts of Ruby is rake and Rails makes extensive use of
it for setting up and managing apps. In the Django ecosystem, many
similar tasks are performed via the ‘manager.py’ script.
Command
Rails
Django
Populate the database
rake db:migrate
manage.py syncdb
Shell access to models
rails console
manage.py shell
Global
Django: “reusable apps”
Rails: “engines” (and to a lesser degree, plugins)
Rails: built in sessions but no authentication or authorization.
Django: sessions + authentication. Not sure how much authorization infrastructure comes built in.
Troubleshooting
rails console - great for models. Now better than it was for
controllers, routes, helpers. Not sure about views. Find a recent blog
post and read up / link.
Django shell:
manage.py shell - great for models. To poke around your views layer
(anything that needs the response object), you need to add some power:
Both Rails and Django make handling associations a breeze. In Django,
for one to many associations, all you need to do is create declare an
attribute on the many side (the side that gets the mapping column)
that is a ForeignKey, e.g.:
And Django sets up the mapping for you. Then you can assign a reporter
to an article by saying my_article.reporter = some_reporter. And you
can chain methods to get to the reporter’s name:
my_article.reporter.full_name. From the reporter side, you can get
a list of all her articles by asking for
murrow.article_set.all(). Rails is similar except that you don’t use
foreign key relationships (not my favorite Rails decision) but instead
you declare the relationships explicitly in both models:
The Django database query interface is a bit like the new ActiveModel
syntax - but with ‘get’ instead of ‘find’.
# Django provides a rich database lookup API.
>>>Reporter.objects.get(id=1)<Reporter:JohnSmith>>>>Reporter.objects.get(full_name__startswith='John')<Reporter:JohnSmith>>>>Reporter.objects.get(full_name__contains='mith')<Reporter:JohnSmith>>>>Reporter.objects.all()[<Reporter:JohnSmith>]
Even in the first part of the tutorial, I run up against Python’s
‘explicit is better than implicit’ philosophy. Rails will use the
class name to infer what instances of your model should be called. You
can get to that value using the human_attribute_name method and you
can either override that method in your model (old school) or use
Rails internationalization capabilities to override the value in your
locale config files. ActiveRecord also provides a default display of
each instance which includes all the attributes of an instance. By
contrast, in Django you need to define a __unicode__() (or in Python 3
a __str__()) method for each model class so that the shell and the
admin interface know what to call things. There doesn’t appear to be a
default representation that shows all the data values; I suspect you
are expected to use Python’s dir() function, e.g. print dir(my_poll).
Models
In Rails the many side of the relationship is accessed as the plural
of it’s name: my_poll.choices. In Django you use type + _set: my_poll.choice_set
Rails scopes (formerly known as named_scopes) let’s you make something
that looks like a method that can be used to constructed queries -
especially useful if you want to chain several of these conditions
together but in a readable way.
Poking around in Django it appears to me that there isn’t such a thing
unless the people answering questions on StackOverflow are as
ignorant as I am about Django’s query interface. The answer appears to
be, if it’s simple, just put the logic for the method directly into
your filter. If it is hairier, possibly write the SQL yourself. ???
Validations? Cool part of Rails. Is there a Django equivalent? or do
you do that validation in the view (aka controller)?
Django admin
Wow! Just wow! Rails scaffold gives you OK CRUD functionality and
there are some plugins that help make things somewhat fancier. But the
out of the box admin functionality in Django is fabulous: sorting,
searching, filtering. The closest thing I know in the Rails ecosystem
is ActiveAdmin, which is nice, but deviates somewhat from the Rails
standard.
One thing I really like a lot is the “History” tab. I end up wanting
to add auditing to a lot of things I write but in several cases I have
added acts_as_versioned to tracked the data - but didn’t provide an
admin interface for displaying the changes. So, of course, now I want
more. Does the Django admin interface provide a way to revert to a
specific version? And can it diff within a text area?
Controllers / Views
Similar render and redirect options. Django provides two really handy
shortcuts get_object_or_404
and [get_list_or_404]((https://docs.djangoproject.com/en/1.8/topics/http/shortcuts/#get-list-or-404).
Handy enough I should create equivalents instead of the controller
code I have.
Routing
Rails’ own interpretation of RESTful urls is strongly favored - in
code and by the community. URLs all defined in a single routes.rb
config file - unless you are using an engine. (For a while engines
didn’t really have a mechanism for creating their routes other than by
having a generator write something into the routes file at
install. But now I think the routing infrastructure will look for
routes files in engines that are mounted into your Rails project.
Django’s routing has a master file, urls.py but it is expected that
each of your site’s apps will define their own urls and that you will
just include those urls into your top level urls.py with a line like
url(r'^polls/', include('polls.urls')) - with or without a
namespace. The point of a namespace is to avoid name conflicts in the
global set of url names. In with the include above, the names in
polls.urls will be global - so the url named details in polls.urls
will be the details url for the entire app. If you say
url(r'^polls/', include('polls.urls', namespace='polls')), then they
will not be global and instead can be accessed as polls:details.
Both frameworks encourage you to use the url lookup mechanism for
creating links instead of hard coding them. The big win for this is
that if your url need to change, you only have to change the
appropriate routes.rb or urls.py file. The references to the urls can
all stay the same.
In both systems, some people like having even shorter syntax for
defining redirects - redirecting to the object. In Rails it is, as
usual, ‘convention over configuration’. If you have defined your
routes using ‘resources’, then you can just say redirect_to object
In Django, you can get a very similar redirect syntax: return
redirect(object) But to make it work, you need to define a
get_absolute_url method on your model class. For example:
Every web framework provides some way of creating web pages from
modular pieces. In fact defining your design and navigation elements
in one place is sometimes the major reason for using a framework in
the first place. I still need to read ‘The Rails View’ to see if I am
doing this right, but I think the Django views may be a bit more
flexible than the normal Rails view processing.
Rails - one big layout file - chose which main layout file by setting
the name in the controller. ‘yield some_name’ and ‘content_for
some_name’. Helpers - methods to munge display variables.
Django general “extends” mechanism. Named blocks. Filters attached to
data using unix-style pipe, “|”.
Helpers
One of the fabulous things about Rails is all the useful helper
methods available to you. Some were created for internal use but are
super handy for display purposes, e.g. humanize, pluralize,
etc. Others are specifically for display. Many other frameworks either
had their own versions, or borrowed liberally from Rails.
Code generation is nice - but there is a lot less typing involved in
declaring the form using the Django admin tool. Field sets are quite
nice - esp with the automatic ‘collapse’ option. I think this is
fairly similar to Rails’ ActiveAdmin. (Sad to see that AA uses Devise
for it’s authenitcation tool.)
Testing
Both frameworks come from a background of TDD and have pretty good
tools for writing tests. Ruby actually has an embaarssment of riches
since it seems that everyone has written their own testing DSL.
Default task runs all tests, or you can run parts. In Rails there are
tests for each layer. For
Django,
the tests are broken out per app: manage.py test polls. View tests
can assert against the content of the page with ‘contains’ and if you
don’t find what you are looking for, you can print out the web page
content:
response=self.client.get(reverse('polls:index'))printresponse.contentself.assertContains(response,"No polls are available.")get:show,:url_parts=>@nested_page.url_partsputsresponse.bodyassert_select'form[action*=page/update]'
Granular testing. Easy to do in rspec_rails. Possible but not so easy
in test_unit/minitest. Easy in Django - just append the name of a
specifit test to your command line:
$./manage.pytestanimals# runs all tests in the animals app$./manage.pytestanimals.AnimalTestCase# runs tests in one class$./manage.pytestanimals.AnimalTestCase.test_animals_can_speak# runs one test method
Database for running tests
Djano: Aside from using a separate database, the test runner will
otherwise use all of the same database settings you have in your
settings file: ENGINE, USER, HOST, etc. The test database is created
by the user specified by USER, so you’ll need to make sure that the
given user account has sufficient privileges to create a new database
on the system.
Both offer fixtures for setting up data. Rails community mainly moved
to factories and the Django tutorial has you create a tiny factory as
a helper methods. Are there libraries for factories in wide use?
Overriding settings - esp urls for apps that could be mounted
anywhere; common option - use a decorator on the test method on or the
entire class: @override_settings(LOGIN_URL=’/other/login/’)
Some useful looking Django test methods:
SimpleTestCase.assertFieldOutput
TestCase.assertFormError
Django integration with Selenium: LiveServerTestCase
I have been using RVM for several years - straight in the terminal and
in a bash shell in emacs. But all of a sudden I started having path
problems. I don’t remember having updated my RVM version, though I
quite possibly I did before I updated my installed rubies for the
latest security patches. Or perhaps there is some odd interaction with
the ruby that got installed when I installed the Heroku toolbelt (v
3.2.0) this last week; it appears to have installed it’s own ruby in
/usr/local/heroku/ruby/bin/ruby but then added /usr/local/heroku/bin
with export PATH="/usr/local/heroku/bin:$PATH" in my .bashrc.
The symptom was that in my bash shell within emacs I was getting
warnings about RVM not finding what it expected at the start of my
path. I should have copied the error message but it was expecting
something like /Users/cnk/.rvm/gems/ruby-2.0.0-p353/bin. Initially I
was getting a path starting with /usr/local/heroku/bin because the
Heroku Toolbelt install had placed that line at the end of my .bashrc
file. But even after having moved the heroku line into my .profile
before the rvm line, I was still getting oddities when trying to use
knive (installed with the chef gem). I tried the fixed suggested in
the warning message: rvm get stable --auto-dotfiles. That gave me
version 1.25.14. and made modifications to my dotfiles:
That made everything work in the terminal but made it much worse in
emacs. With the default rvm setup (source "$HOME/.rvm/scripts/rvm" in
my .bash_profile file), rvm was not getting loaded in my emacs
shell. type -t rvm was returning ‘file’ instead of ‘function’ and
none of my rvm commands would work - no rvm use or rvm gemset
list. I fooled around for a bit and sorted out that what I need to
get rvm to work in the terminal AND in my emacs shell is to have this
line in my .bashrc file NOT in .bash_profile or .profile:
# Load RVM into a shell session *as a function*[[-s"$HOME/.rvm/scripts/rvm"]]&&source"$HOME/.rvm/scripts/rvm"
That works fine and does not seem to have any problems even if I
create a new shell within an old one.
Other RVM and Emacs Options
There are a couple of more sophisticated ways of using rvm within
emacs that I came across while trying to figure out why this had
suddenly stopped working. One of these days I should look at them:
I got the same warning again today - when changing directories from
within a shell running in emacs:
[04:12PM](brazen:~/chef-tdd) $ rvm list
Warning!PATHisnotproperlysetup,'/Users/cnk/.rvm/gems/ruby-2.0.0-p353/bin'isnotavailable,usuallythisiscausedbyshellinitializationfiles-checkthemfor'PATH=...'entries,itmightalsohelptore-addRVMtoyourdotfiles:'rvmgetstable--auto-dotfiles',tofixtemporarilyinthisshellsessionrun:'rvmuseruby-2.0.0-p353'.rvmrubiesruby-1.9.3-p374[x86_64]ruby-1.9.3-p484[x86_64]ruby-2.0.0-p0[x86_64]=*ruby-2.0.0-p353[x86_64]# => - current# =* - current && default# * - default
This time a) I remembered to record it and b) I read the full message!
I went with the last option: rvm use ruby-2.0.0-p353. Running that
command got me the same warning again - but once the command executed,
then all was repaired and the rest of the commands work fine, e.g. rvm
list, rvm gemset list, etc.