In an earlier post we described a simple Topaz application that could be pushed to Cloud Foundry. It turns out that the example in that post did not handle multiple instances very well since each time the app started it would reinstall WebTools. Since the previous classes were removed as part of the subsequent install, this left the earlier instances looking for subclasses that no longer existed. In this example we obtain a lock on the user’s SymbolList to ensure that only one session attempts the install. Then we create an install script that only does the install if things are not yet present. Finally, we remove the lock before starting the web server.

run
[
  [
    System writeLock: System myUserProfile symbolList.
    true.
  ] on: Error do: [:ex | 
    ex return: false.
  ].
] whileFalse: [
  (Delay forSeconds: 1) wait.
  System abortTransaction.
].
true
%
run
| gsFile |
gsFile := GsFile openWriteOnServer: 'install.tpz'.
System myUserProfile symbolList last name == #'WebTools' ifFalse: [
  gsFile nextPutAll: 'input $GEMSTONE/examples/www/install.tpz'; lf.
].
gsFile close.
true
%
input install.tpz
run
System removeLock: System myUserProfile symbolList.
true.
%
run
| class port |
class := GsSession currentSession userProfile symbolList last at: #'Server'.
port := System gemEnvironmentVariable: 'VCAP_APP_PORT'.
class new 
  password: 'swordfish';
  startForegroundServerAtPort: port asNumber.
%
logout
exit

Once you have thing running if you have two instances you can try the following in a workspace and see what changes:

System myUserProfile userId. "This should not change"
System performOnServer: 'whoami; hostname; pwd'. "This might change"

We will discuss this example in a presentation Wednesday morning at STIC 2012.

In our last post we walked through adding GemStone/S as a service in Cloud Foundry. With this, we could access GemStone/S from an application written in a runtime/framework recognized by Cloud Foundry. Now, we look at what it takes to have GemStone/S recognized as a runtime/framework (named ‘topaz’) so we can work directly in server Smalltalk.

Changes to VMC on the Client

First, we need to make changes on the client so that VMC recognizes topaz as a framework. This process is discussed in detail here and involves a couple small edits to lib/cli/frameworks.rb. Insert the following at line 9 (to the list of FRAMEWORKS):

'Topaz' => ['topaz', { :mem => '256M', :description => 'Topaz for GemStone/S'}],

Then we insert the following detection code staring at line 42 (after the Rails detection):

# Topaz on GemStone
elsif File.exist?('main.tpz')
  return Framework.lookup('Topaz')

Changes to VCAP on the Server

First we navigate to the VCAP location and create some directories:

cd ~/cloudfoundry/vcap/
mkdir ./dev_setup/cookbooks/topaz
mkdir ./dev_setup/cookbooks/topaz/recipes
mkdir ./dev_setup/cookbooks/topaz/attributes
mkdir ./staging/lib/vcap/staging/plugin/topaz

The template for the topaz script at dev_setup/cookbooks/gemstone/templates/default/topaz.erb that we created earlier needs a few changes and should be replaced with the following:

#!/bin/bash
if [[ "$1" == "-v" ]]; then
  echo "<%= node[:gemstone][:version] %>"
  exit 0
fi
export GEMSTONE=<%= node[:gemstone][:path] %>/product
export PATH=$GEMSTONE/bin:$PATH
export GEMSTONE_GLOBAL_DIR=<%= node[:gemstone][:service_dir] %>
topaz -l \
  -I <%= node[:gemstone][:service_dir] %>/bin/.topazini \
  -e <%= node[:gemstone][:service_dir] %>/etc/topaz.conf \
  -z <%= node[:gemstone][:service_dir] %>/etc/system.conf $*

Next we need to modify the provisioning script at services/gemstone/resources/provision.tpz.erb to remove the NoGsFile* privileges so that we can write to stdout from Smalltalk. File-level security can be enforced by Cloud Foundry by setting ‘secure: true‘ in dea/config/dea.yml and starting VCAP as root (e.g., with sudo).

Next we modify cloud_controller/app/models/app.rb lines 26, 27 to add topaz as a runtime and framework:

Runtimes = %w[topaz ruby18 ruby19 java node php erlangR14B02 python26]
Frameworks = %w[topaz sinatra rails3 java_web spring grails node php otp_rebar lift wsgi django unknown]

Next we modify dev_setup/cookbooks/cloud_controller/attributes/default.rb to insert the following at line 10:

default[:cloud_controller][:staging][:topaz] = "topaz.yml"

Next we modify 
dev_setup/cookbooks/cloud_controller/templates/default/cloud_controller.yml.erb to insert the following at line 110:

topaz:
 version: 3.0.1

Next we create dev_setup/cookbooks/cloud_controller/templates/default/topaz.yml.erb with the following:

---
name: "topaz"
runtimes:
  - "topaz":
      version: "3.0.1"
      description: "Topaz"
      executable: "/var/vcap/services/gemstone/bin/topaz"
      default: true
      environment:
detection:
  - "main.tpz"

Next we modify dev_setup/cookbooks/dea/attributes/default.rb to add “topaz” to line 3:

default[:dea][:runtimes] = ["topaz", "ruby18", "ruby19", "nodejs", "java", "erlang", "php"]

Next we modify dev_setup/cookbooks/dea/templates/default/dea.yml.erb to insert the following at line 46:

<% if node[:dea][:runtimes].include?("topaz") %>
  topaz:
    executable: <%= File.join(node[:topaz][:path]) %>
    version: 3.0.1
    environment:
<% end %>

Next we create dev_setup/cookbooks/topaz/attributes/default.rb with the following:

include_attribute "deployment"
default[:topaz][:version] = "3.0.1"
default[:topaz][:path] = "/var/vcap/services/gemstone/bin/topaz"

Next we create dev_setup/cookbooks/topaz/recipes/default.rb with the following:

# 
# Cookbook Name:: topaz
# Recipe:: default 
# 
# Copyright 2012, VMware 
# 
Chef::Log.debug("Topaz should have been installed with GemStone/S 64 Bit")

Next we create staging/lib/vcap/staging/plugin/manifests/topaz.yml with the following:

---
name: "topaz"
runtimes:
  - topaz:
      version: '3.0.1'
      description: 'Topaz for GemStone/S'
      executable: /var/vcap/services/gemstone/bin/topaz
      default: true
app_servers:
detection:
  - "main.tpz": '.'
staged_services:

Next we create staging/lib/vcap/staging/plugin/topaz/plugin.rb with the following:

class TopazPlugin < StagingPlugin
  include GemfileSupport
  def framework
    'topaz'
  end
#
  def stage_application
    Dir.chdir(destination_directory) do
      create_app_directories
      copy_source_files
      do_staging
      create_startup_script
      create_stop_script
    end
  end
#
  def do_staging
    Dir.chdir("app") {|dir|
      # nothing really to do here!
    }
  end
#
  def start_command
    "/var/vcap/services/gemstone/bin/topaz -I .topazini < main.tpz"
  end
#
  private
  def startup_script
    vars = environment_hash
    # PWD here is the parent of the 'app' directory.
    generate_startup_script(vars) do
      cmds = []
      cmds << 'USER=$(echo $VCAP_SERVICES | ' + \
        'grep -Po \'"user":.*?[^\\\\]",\' | cut -d\'"\' -f4)'
      cmds << 'PASS=$(echo $VCAP_SERVICES | ' + \
        'grep -Po \'"pass":.*?[^\\\\]",\' | cut -d\'"\' -f4)'
      cmds << 'HOST=$(echo $VCAP_SERVICES | ' + \
        'grep -Po \'"host":.*?[^\\\\]",\' | cut -d\'"\' -f4)'
      cmds << 'echo "set user $USER pass $PASS" > app/.topazini'
      cmds << 'echo "set gems !tcp@$HOST#server!gs64stone" >> app/.topazini'
      cmds << 'echo "login" >> app/.topazini'
      cmds.join("\n")
    end
  end
#
  def stop_script
    vars = environment_hash
    generate_stop_script(vars)
  end
#
end

Next we create staging/lib/vcap/staging/plugin/topaz/staging with the following:

#!/usr/bin/env ruby
require File.expand_path('../../common', __FILE__)
plugin_class = StagingPlugin.load_plugin_for('topaz')
plugin_class.validate_arguments!
plugin_class.new(*ARGV).stage_application

Our demo application will be of WebTools, a “goodie” provided with GemStone/S, but it needs a couple fixes to work with Cloud Foundry. Our earlier post shows how to install GemStone/S and the Chef script installs the product tree at ~/cloudfoundry/.deployments/devbox/deploy/gemstone/product/. The changes need to be made to examples/www/src/Server.gs (I would give the full path but it doesn’t display well with the blog formatting). In the #’doAnswer’ method we need to replace the #’lf’ message sends with #’crlf’ so that we get a CR/LF as required by the HTTP spec. In the #’handleRequest’ method we need to remove the check for HTTP/1.1 since Cloud Foundry sends the requests using a 1.0 format.

At this point we can deploy Cloud Foundry:

~/cloudfoundry/vcap/dev_setup/bin/vcap_dev_setup -d ~/cloudfoundry/

As we found earlier, this doesn’t properly install the staging code, so we copy it across manually:

fromDir=`find ~/cloudfoundry/vcap/staging/ | grep pip_support.rb`
fromDir=`dirname $fromDir`
toDir=`find ~/cloudfoundry/.deployments/devbox/ | grep pip_support.rb`
toDir=`dirname $toDir`
cp -r $fromDir/topaz $toDir
cp $fromDir/manifests/topaz.yml $toDir/manifests/topaz.yml

We can now start Cloud Foundry, vcap start, and return to the client to create a new GemStone/S application.

Creating an Application

We need to have an application to push to the cloud, so we will start by creating a directory to hold the application on the client:

mkdir ~/cloud/topaz
cd ~/cloud/topaz
vim ~/cloud/topaz/main.tpz

Our application, main.tpz, will consist of the following Topaz script:

input $GEMSTONE/examples/www/install.tpz
run
| class port |
class := GsSession currentSession userProfile symbolList last at: #'Server'.
port := System gemEnvironmentVariable: 'VCAP_APP_PORT'.
class new startForegroundServerAtPort: port asNumber.
%
logout
exit

At this point we can push our application, give it a name, and bind it to a gemstone service:

Would you like to deploy from the current directory? [Yn]: 
Application Name: topaz-app
Application Deployed URL [topaz-app.vcap.me]: 
Detected a Topaz for GemStone/S, is this correct? [Yn]: 
Memory reservation (128M, 256M, 512M, 1G, 2G) [256M]: 
How many instances? [1]: 
Would you like to bind any services to 'topaz-app'? [yN]: y
The following system services are available
1: gemstone
2: mongodb
3: mysql
4: neo4j
5: redis
Please select the one you wish to provision: 1
Specify the name of the service [gemstone-1b271]: 
Would you like to bind another service? [yN]: 
Would you like to save this configuration? [yN]: 
Creating Application: OK
Creating Service [gemstone-1b271]: OK
Binding Service [gemstone-1b271]: OK
Uploading Application:
 Checking for available resources: OK
 Packing application: OK
 Uploading (0K): OK 
Push Status: OK
Staging Application 'topaz-app': OK 
Starting Application 'topaz-app': OK

When the application has been started, we can navigate to http://topaz-app.vcap.me/ and see that we are interacting with a GemStone/S system over the web.

In our previous post we used Chef to install GemStone/S 64 Bit into Cloud Foundry. Now we add code so that GemStone is recognized as a service offered by Cloud Foundry. On the server, we start by adding “gemstone” to the cloud_controller by modifying 
~/cloudfoundry/vcap/dev_setup/cookbooks/cloud_controller/attributes/default.rb line 22:

default[:cloud_controller][:builtin_services] = ["gemstone", "redis", "mongodb", "mysql", "neo4j"]

Create ~/cloudfoundry/vcap/bin/services/gemstone_gateway with:

#!/usr/bin/env ruby
# Copyright (c) 2012 VMware, Inc.
#
exec(File.expand_path("../../../services/gemstone/bin/gemstone_gateway",
  __FILE__), *ARGV)

Create ~/cloudfoundry/vcap/bin/services/gemstone_node with:

#!/usr/bin/env ruby
# Copyright (c) 2012 VMware, Inc.
#
exec(File.expand_path("../../../services/gemstone/bin/gemstone_node",
  __FILE__), *ARGV)

Set these two files to be executable:

chmod 0755 ~/cloudfoundry/vcap/bin/services/gemstone_*

The rest of the work is creating following directories and files to manage the service:

  • ~/cloudfoundry/vcap/services/gemstone/
    • bin/
      • gemstone_gateway
      • gemstone_node
    • config/
      • gemstone_gateway.yml
      • gemstone_node.yml
    • lib/
      • gemstone_service/
        • common.rb
        • error.rb
        • node.rb
        • provisioner.rb
    • resources/
      • provision.tpz.erb
      • unprovision.tpz.erb
    • Gemfile
    • Gemfile.lock
    • Rakefile
    • README
    • .gitignore

We create the directory structure:

mkdir ~/cloudfoundry/vcap/services/gemstone
cd ~/cloudfoundry/vcap/services/gemstone
mkdir bin config lib lib/gemstone_service resources

Create Gemfile with:

source "http://rubygems.org"
#
gem "nats"
gem "datamapper", ">= 0.10.2"
gem "do_sqlite3", :require => nil
gem "dm-sqlite-adapter"
gem "eventmachine"
gem "em-http-request"
gem "json"
gem "mysql"
gem "uuidtools"
gem "ruby-hmac", :require => "hmac-sha1"
gem "thin"
gem "sinatra"
#
gem 'vcap_common', '~> 1.0.3', :require => ['vcap/common', 'vcap/component']
gem 'vcap_logging', '>=0.1.3', :require => ['vcap/logging']

Create Gemfile.lock with:

GEM
  remote: http://rubygems.org/
  specs:
    addressable (2.2.4)
    bcrypt-ruby (2.1.4)
    daemons (1.1.5)
    data_objects (0.10.3)
      addressable (~> 2.1)
  datamapper (1.1.0)
      dm-aggregates (= 1.1.0)
      dm-constraints (= 1.1.0)
      dm-core (= 1.1.0)
      dm-migrations (= 1.1.0)
      dm-serializer (= 1.1.0)
      dm-timestamps (= 1.1.0)
      dm-transactions (= 1.1.0)
      dm-types (= 1.1.0)
      dm-validations (= 1.1.0)
    dm-aggregates (1.1.0)
      dm-core (~> 1.1.0)
    dm-constraints (1.1.0)
      dm-core (~> 1.1.0)
    dm-core (1.1.0)
      addressable (~> 2.2.4)
    dm-do-adapter (1.1.0)
      data_objects (~> 0.10.2)
      dm-core (~> 1.1.0)
    dm-migrations (1.1.0)
      dm-core (~> 1.1.0)
    dm-serializer (1.1.0)
      dm-core (~> 1.1.0)
      fastercsv (~> 1.5.4)
      json (~> 1.4.6)
    dm-sqlite-adapter (1.1.0)
      dm-do-adapter (~> 1.1.0)
      do_sqlite3 (~> 0.10.2)
    dm-timestamps (1.1.0)
      dm-core (~> 1.1.0)
    dm-transactions (1.1.0)
      dm-core (~> 1.1.0)
    dm-types (1.1.0)
      bcrypt-ruby (~> 2.1.4)
      dm-core (~> 1.1.0)
      fastercsv (~> 1.5.4)
      json (~> 1.4.6)
      stringex (~> 1.2.0)
      uuidtools (~> 2.1.2)
    dm-validations (1.1.0)
      dm-core (~> 1.1.0)
    do_sqlite3 (0.10.3)
      data_objects (= 0.10.3)
    em-http-request (0.3.0)
      addressable (>= 2.0.0)
      escape_utils
      eventmachine (>= 0.12.9)
    escape_utils (0.2.3)
    eventmachine (0.12.11.cloudfoundry.3)
    fastercsv (1.5.4)
    json (1.4.6)
    json_pure (1.6.4)
    little-plugger (1.1.3)
    logging (1.6.1)
      little-plugger (>= 1.1.2)
    mysql (2.8.1)
    nats (0.4.22.beta.4)
      daemons (>= 1.1.4)
      eventmachine (>= 0.12.10)
      json_pure (>= 1.6.1)
      thin (>= 1.3.1)
    posix-spawn (0.3.6)
    rack (1.4.0)
    ruby-hmac (0.4.0)
    sinatra (1.2.1)
      rack (~> 1.1)
      tilt (>= 1.2.2, < 2.0)
    stringex (1.2.1)
    thin (1.3.1)
      daemons (>= 1.0.9)
      eventmachine (>= 0.12.6)
      rack (>= 1.0.0)
    tilt (1.2.2)
    uuidtools (2.1.2)
    vcap_common (1.0.3)
      eventmachine (~> 0.12.11.cloudfoundry.3)
      logging (>= 1.5.0)
      nats (~> 0.4.22.beta.4)
      posix-spawn (~> 0.3.6)
      thin (~> 1.3.1)
      yajl-ruby (~> 0.8.3)
    vcap_logging (0.1.3)
    yajl-ruby (0.8.3)
#
PLATFORMS
  ruby
#
DEPENDENCIES
  datamapper (>= 0.10.2)
  dm-sqlite-adapter
  do_sqlite3
  em-http-request
  eventmachine
  json
  mysql
  nats
  ruby-hmac
  sinatra
  thin
  uuidtools
  vcap_common (~> 1.0.3)
  vcap_logging (>= 0.1.3)

Create .gitignore with:

*.db

Create Rakefile with:

require 'rake'
#
desc "Run specs"
task "spec" => ["bundler:install:test", "test:spec"]
#
desc "Run specs using RCov"
task "spec:rcov" => ["bundler:install:test", "test:spec:rcov"]
#
namespace "bundler" do
  desc "Install gems"
  task "install" do
    sh("bundle install")
  end
#
  desc "Install gems for test"
  task "install:test" do
    sh("bundle install --without development production")
  end
#
  desc "Install gems for production"
  task "install:production" do
    sh("bundle install --without development test")
  end
#
  desc "Install gems for development"
  task "install:development" do
    sh("bundle install --without test production")
  end
end
#
namespace "test" do
  task "spec" do |t|
    sh("cd spec && ../../base/bin/nats-util start &&"
      " rake spec && ../../base/bin/nats-util stop")
  end
#
  task "spec:rcov" do |t|
    sh("cd spec && rake spec:rcov")
  end
end

Create README with:

This code provides GemStone/S 64 Bit as a Cloud Foundry service.

Create bin/gemstone_gateway with:


#!/usr/bin/env ruby
# -*- mode: ruby -*-
#
# Copyright (c) 2009-2011 VMware, Inc.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path('../../Gemfile', __FILE__)
#
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', '..', 
  'base', 'lib')
require 'base/gateway'
#
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
require 'gemstone_service/provisioner'
#
class VCAP::Services::Gemstone::Gateway < VCAP::Services::Base::Gateway
#
  def provisioner_class
    VCAP::Services::Gemstone::Provisioner
  end
#
  def default_config_file
    config_base_dir = ENV["CLOUD_FOUNDRY_CONFIG_PATH"] || 
      File.join(File.dirname(__FILE__), '..', 'config')
    File.join(config_base_dir, 'gemstone_gateway.yml')
  end
#
end
#
VCAP::Services::Gemstone::Gateway.new.start

Create bin/gemstone_node with:

#!/usr/bin/env ruby
# -*- mode: ruby -*-
# Copyright (c) 2009-2011 VMware, Inc.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
#
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', '..', 
  'base', 'lib')
require 'base/node_bin'
#
$LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
require "gemstone_service/node"
#
class VCAP::Services::Gemstone::NodeBin < VCAP::Services::Base::NodeBin
#
  def node_class
    VCAP::Services::Gemstone::Node
  end
#
  def default_config_file
    config_base_dir = ENV["CLOUD_FOUNDRY_CONFIG_PATH"] || 
      File.join(File.dirname(__FILE__), '..', 'config')
    File.join(config_base_dir, 'gemstone_node.yml')
  end
#
  def additional_config(options, config)
    options
  end
#
end
#
VCAP::Services::Gemstone::NodeBin.new.start

Change these two files to be executable:

chmod 0755 bin/gemstone_*

Create config/gemstone_gateway.yml with:

---
#cloud_controller_uri: api.vcap.me
service:
  name: gemstone
  version: "3.0"
  description: 'GemStone/S 64 Bit database service'
  plans: ['free']
  tags: ['gemstone', 'gemstone-3.0', 'object']
ip_route: localhost
index: 0
token: "0xdeadbeef"
logging:
  level: debug
mbus: nats://localhost:4222
pid: /var/vcap/sys/run/gemstone_service.pid
node_timeout: 2

Create config/gemstone_node.yml with:

---
local_db: sqlite3:/var/vcap/services/gemstone/gemstone_node.db
base_dir: /var/vcap/services/gemstone/
ip_route: 127.0.0.1
mbus: nats://localhost:4222
index: 0
logging:
  level: debug
pid: /var/vcap/sys/run/gemstone_node.pid
available_storage: 1024
node_id: gemstone_node_1
migration_nfs: /mnt/migration
gemstone:
  host: localhost
  port: 50377
  user: gemroot
  pass: gemroot

Create lib/gemstone_service/common.rb with:

# Copyright (c) 2012 VMware, Inc.
module VCAP
  module Services
    module Gemstone
      module Common
        def service_name
          "GaaS"
        end
      end
    end
  end
end

Create lib/gemstone_service/error.rb with:

# Copyright (c) 2012 VMware, Inc.
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', '..', '..', 
  'base', 'lib')
#
require "base/service_error"
#
class VCAP::Services::Gemstone::GemstoneError<
    VCAP::Services::Base::Error::ServiceError
  GSS_LOCAL_DB_ERROR = [1, HTTP_INTERNAL, 'Problem with CF database']
end

Create lib/gemstone_service/provisioner.rb with:

# Copyright (c) 2012 VMware, Inc.
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', '..', '..', 
  'base', 'lib')
#
require 'base/provisioner'
require 'gemstone_service/common'
#
class VCAP::Services::Gemstone::Provisioner < 
  VCAP::Services::Base::Provisioner
#
  include VCAP::Services::Gemstone::Common
#
  def node_score(node)
    10 # > 0 for ~/cloudfoundry/vcap/services/base/lib/base/provisioner.rb
  end
#
end

Create lib/gemstone_service/node.rb with:

# Copyright (c) 2012 VMware, Inc.
require "erb"
require "fileutils"
require "logger"
require "pp"
#
require "uuidtools"
require "open3"
require "thread"
#
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', '..', '..', 
  'base', 'lib')
require 'base/node'
require 'base/service_error'
require "datamapper_l"
#
module VCAP
  module Services
    module Gemstone
      class Node < VCAP::Services::Base::Node
      end
    end
  end
end
#
require "gemstone_service/common"
require "gemstone_service/error"
#
class VCAP::Services::Gemstone::Node
#
  include VCAP::Services::Gemstone::Common
  include VCAP::Services::Gemstone
#
  # ProvisionedService is the data stored by CloudFoundry for a provisioned
  # instance of the GemstoneService. This info is made available throughout
  # the system, e.g., at the time an app is staged / run.
  class ProvisionedService
    include DataMapper::Resource
#
    property :name, String, :key => true
    property :plan, Enum[:free], :required => true
    property :user, String, :required => true
    property :pass, String, :required => true
  end
#
  # +options+ includes the info in ../../config/gemstone_node.yml
  def initialize(options)
    super(options) # handles @node_id, @logger, @local_ip, @node_nats
#   @logger.debug("initialize( #{options.keys.inspect}")
    template_path = File.expand_path('../../resources/provision.tpz.erb', 
      File.dirname(__FILE__))
    @provision_template = ERB.new(File.read(template_path))
    template_path = File.expand_path('../../resources/unprovision.tpz.erb', 
      File.dirname(__FILE__))
    @unprovision_template = ERB.new(File.read(template_path))
    start_local_db(options[:local_db])
    @GEMSTONE = options[:base_dir]
    start_gemstone
  end
#
  def start_local_db(local_db)
    DataMapper.setup(:default, local_db)
    DataMapper::auto_upgrade!
  end
#
  def start_gemstone
    `#{@GEMSTONE}/bin/startstone`
  end
#
  def shutdown
    @logger.debug("shutdown")
    `#{@GEMSTONE}/bin/stopstone`
    super
  end
def pre_send_announcement
#   @logger.debug("pre_send_announcement")
    super
  end
def announcement
# @logger.debug("announcement")
    a = {
      :some_random_data => 42,
    }
  end
#
  def provision(plan, credential=nil)
    @logger.info("provision: plan #{plan} credential: #{credential.inspect}")
    provisioned_service = ProvisionedService.new
    provisioned_service.plan = plan
    if credential
      provisioned_service.name = credential[:name]
      provisioned_service.user = credential[:user]
      provisioned_service.pass = credential[:password]
    else
      provisioned_service.name = 'GS-' + 
        UUIDTools::UUID.random_create.to_s.gsub(/-/, '')
      provisioned_service.user = 'CF-' + generate_credential
      provisioned_service.pass = generate_credential
    end
#
    `#{@GEMSTONE}/bin/topaz_dc <<EOF
#{@provision_template.result(binding)}
EOF`
#
    if not provisioned_service.save
      @logger.error("Could not save entry: " \
        "#{provisoned_service.errors.inspect}")
      raise GemstoneError.new(GemstoneError::GSS_LOCAL_DB_ERROR)
    end
#
    response = {
      "name" => provisioned_service.name,
      "user" => provisioned_service.user,
      "pass" => provisioned_service.pass,
      "host" => @local_ip,
      "port" => 0,
    }
  end
#
  def unprovision(name, credentials)
    @logger.info("unprovision: name #{name} " \
      "credentials: #{credentials.inspect}")
    return if name.nil?
    provisioned_service = ProvisionedService.get(name)
    raise GemstoneError.new(GemstoneError::GSS_LOCAL_DB_ERROR) \
      if provisioned_service.nil?
#
    `#{@GEMSTONE}/bin/topaz_dc <<EOF
#{@unprovision_template.result(binding)}
EOF`
#
    if not provisioned_service.destroy
      @logger.error("Could not delete service: " \
        "#{provisioned_service.errors.inspect}")
      raise GemstoneError.new(GemstoneError::GSS_LOCAL_DB_ERROR)
    end
  end
#
  def bind(name, bind_opts, credential=nil)
    @logger.debug("Bind request: name=#{name}, \
      bind_opts=#{bind_opts}, credential=#{credential.inspect}")
    provisioned_service = ProvisionedService.get(name)
    raise GemstoneError.new(GemstoneError::GSS_LOCAL_DB_ERROR) \
      if provisioned_service.nil?
    response = {
      "user" => provisioned_service.user,
      "pass" => provisioned_service.pass,
      "host" => @local_ip,
      "port" => 0,
    }
  end
#
  def unbind(credentials)
    @logger.debug("Unbind request: credentials=#{credentials}")
  end
#
  CREDENTIAL_CHARACTERS = 
    ("A".."Z").to_a + ("a".."z").to_a + ("0".."9").to_a
  def generate_credential(length=12)
    Array.new(length) { 
      CREDENTIAL_CHARACTERS[rand(CREDENTIAL_CHARACTERS.length)] }.join
  end
#
end

Create resources/provision.tpz.erb with:

! In GemStone/S 64 Bit we respond to a provisioning request by adding a user
! output pushnew provision.out
!
run
| securityPolicy newUser |
securityPolicy := (GsObjectSecurityPolicy newInRepository: SystemRepository)
  ownerAuthorization: #'write';
  worldAuthorization: #'none';
  yourself.
System commitTransaction.
newUser := AllUsers 
  userWithId: '<%= provisioned_service.user %>' 
  ifAbsent: [ nil ].
newUser ~~ nil ifTrue: [
  AllUsers removeAndCleanup: newUser.
  System commitTransaction.
].
newUser := AllUsers
  addNewUserWithId: '<%= provisioned_service.user %>'
  password: '<%= provisioned_service.pass %>'
  defaultObjectSecurityPolicy: securityPolicy
  privileges: #(
    #'CodeModification' 
    #'NoPerformOnServer' 
    #'NoUserAction'
    #'NoGsFileOnServer'
    #'NoGsFileOnClient')
  inGroups: #().
securityPolicy owner: newUser.
System commitTransaction.
%
logout
exit

Create resources/unprovision.tpz.erb with:

! We respond to an uprovisioning request by removing a user
! output pushnew unprovision.out
!
run
| user |
user := AllUsers 
  userWithId: '<%= provisioned_service.user %>' 
  ifAbsent: [^true].
AllUsers removeAndCleanup: user.
System commitTransaction.
%
logout
exit

At this point we can repeat the deployment of VCAP:


~/cloudfoundry/vcap/dev_setup/bin/vcap_dev_setup -d ~/cloudfoundry/
~/cloudfoundry/vcap/dev_setup/bin/vcap_dev start

If the startup process reports that it cannot find the correct version of datamapper, then stop vcap, edit ~/cloudfoundry/vcap/services/gemstone/Gemfile.lock, change (1.2.0) to (1.1.0), and restart vcap.

Trying it out

From the client we can verify that the service exists:

vmc services

This shows GemStone/S as a service along with MongoDB, MySQL, Neo4j, and Redis. We can verify that Cloud Foundry handles the new service by pushing our original Ruby application (created here) and adding a gemstone service as part of the push process. Once the application is running, go to a browser and navigate to http://ruby-env.vcap.me/env. In the environment list we see something like the following:

VCAP_SERVICES: {
 "gemstone-3.0":[{
 "name":"gs-env",
 "label":"gemstone-3.0",
 "plan":"free",
 "tags":["gemstone","object"],
 "credentials":{
 "user":"CF-1vztH87mLJhW",
 "pass":"J47kFHmC2CvY",
 "host":"192.168.9.155",
 "port":0
 }
 }]
}

We are particularly interested in the user and password provided as part of the credentials. Returning to the server, we can verify that GemStone is running and the user/password provided is valid:

/var/vcap/services/gemstone/bin/topaz <<EOF
set user CF-1vztH87mLJhW pass J47kFHmC2CvY
login
run
100 factorial asString size
%
logout
exit
EOF

This demonstrates that we have provisioned a new service, bound it to the new application, and that an application could use the provided credentials to connect to GemStone/S and execute Smalltalk code.

The first step in providing GemStone/S 64 Bit as a Cloud Foundry service is having it installed with Cloud Foundry. The new “experimental” vcap_dev_setup script uses Chef to install various components and we will install GemStone/S this way in our private cloud (created here). These steps will all take place in a shell on the server.

Edit ~/cloudfoundry/vcap/dev_setup/lib/job_manager.rb to add “gemstone” to the list of services in line 19.

SERVICES = ["gemstone", "redis", "mysql", "mongodb", "neo4j"]

Create ~/cloudfoundry/vcap/dev_setup/roles/gemstone_gateway.json with the following:

{
  "name": "gemstone_gateway",
  "override_attributes": {},
  "json_class": "Chef::Role",
  "description": "GemStone/S services gateway",
  "chef_type": "role",
  "run_list" : ["recipe[deployment]",
  "recipe[essentials]",
  "recipe[ruby]",
  "recipe[gemstone::gateway]"]
}

Create ~/cloudfoundry/vcap/dev_setup/roles/gemstone_node.json with the following:

{
  "name": "gemstone",
  "override_attributes": {},
  "json_class": "Chef::Role",
  "description": "GemStone/S database for apps",
  "chef_type": "role",
  "run_list" : ["recipe[deployment]",
  "recipe[essentials]",
  "recipe[ruby]",
  "recipe[gemstone]",
  "recipe[gemstone::node]"]
}

We will create the following directories and files:

  •  ~/cloudfoundry/vcap/dev_setup/cookbooks/gemstone/
    • attributes/
      • default.rb
    • recipes/
      • default.rb
      • gateway.rb
      • node.rb
    • templates/
      • default/
        • datacurator.tpz.erb
        • gemstone_gateway.yml.erb
        • gemstone_node.yml.erb
        • gs64stone.conf.erb
        • gslist.erb
        • reset_passwords.tpz.erb
        • startstone.erb
        • stopstone.erb
        • topaz.conf.erb
        • topaz_dc.erb
        • topaz.erb
        • .topazini.erb

Create the Chef cookbook directories:

mkdir ~/cloudfoundry/vcap/dev_setup/cookbooks/gemstone/
cd ~/cloudfoundry/vcap/dev_setup/cookbooks/gemstone/
mkdir attributes recipes templates templates/default

Create attributes/default.rb with the following (with changes to the last line to give a good password):

default[:gemstone][:version] = "3.0.1"
default[:gemstone][:gs64ldi] = "50377"
default[:gemstone][:path] = File.join(node[:deployment][:home],
  "deploy", "gemstone")
default[:gemstone][:service_dir] = "/var/vcap/services/gemstone"
# the following provide a max of 512 MB (okay for a 1 GB machine)
default[:gemstone][:shmmax] = "536870912"
default[:gemstone][:shmall] = "131072"
default[:gemstone_node][:available_memory] = "4096"
default[:gemstone_node][:index] = "0"
default[:gemstone_node][:max_memory] = "128"
default[:gemstone_node][:token] = "changegemstonetoken"
default[:gemstone_node][:spc_size_kb] = "131072"
default[:gemstone_node][:tempobj_cache_size] = "131072"
default[:gemstone_node][:stonename] = "gs64stone"
# generate a good replacement for the following
#  (e.g., http://www.random.org/passwords/)
default[:gemstone_node][:password] = "swordfish"

Create recipes/default.rb with the following:

#
# Cookbook Name:: gemstone
# Recipe:: default
#
# Copyright 2012, VMware
#
#
case node['platform']
when "ubuntu"
  gs64ldi = node[:gemstone][:gs64ldi]
  bash "Add gs64ldi to /etc/services" do
    user "root"
    code <<-EOH
      echo "gs64ldi #{gs64ldi}/tcp # GemStone/S" >> /etc/services
    EOH
    not_if do 
      string = `grep -w gs64ldi /etc/services | xargs echo | \
        cut -d" " -f2 | cut -d"/" -f1`.strip
      !string.empty? && Integer(string) == Integer(gs64ldi)
    end
  end
#
  # Shared Memory
  shmmax = node[:gemstone][:shmmax]
  shmall = node[:gemstone][:shmall]
  bash "Set kernel parameters for shared memory for GemStone/S 64 Bit" do
    user "root"
    code <<-EOH
      sysctl -w kernel.shmmax=#{shmmax}
      sysctl -w kernel.shamll=#{shmall}
      echo "kernel.shmmax = #{shmmax}" >> /etc/sysctl.conf
      echo "kernel.shmall = #{shmall}" >> /etc/sysctl.conf
    EOH
    not_if do 
      Integer(shmmax) <= Integer(`sysctl kernel.shmmax | cut -d" " -f3`)
    end
  end
#
  # Download product tree
  dir_name = "GemStone64Bit#{node[:gemstone][:version]}-x86_64.Linux"
  file_name = dir_name + ".zip"
  bash "Download GemStone/S 64 Bit" do
    cwd File.join("", "tmp")
    user node[:deployment][:user]
    code <<-EOH
      ftp -inv ftp.gemstone.com <<-FTP_END
        user anonymous swordfish
        cd /pub/GemStone64/#{node[:gemstone][:version]}/
        binary
        get #{file_name}
        bye
      FTP_END
    EOH
    not_if do
      ::File.exists?(File.join("", "tmp", file_name))
    end
  end
#
  bash "Install GemStone/S 64 Bit" do
    user node[:deployment][:user]
    group node[:deployment][:user]
    cwd File.join("", "tmp")
    code <<-EOH
      unzip -q #{file_name} -d #{node[:gemstone][:path]}
      ln -s #{node[:gemstone][:path] + "/" + dir_name} \
        #{node[:gemstone][:path]}/product
    EOH
    not_if do
      ::File.exists?(File.join(node[:gemstone][:path], dir_name , \
        "bin", "startstone"))
    end
  end
#
  ["", "bin", "data", "etc", "locks", "log"].each do | dir |
    directory File.join(node[:gemstone][:service_dir], dir) do
      owner node[:deployment][:user]
      group node[:deployment][:user]
      mode "0755"
    end
  end
#
  %w(gs64stone topaz).each do | name |
    template File.join(node[:gemstone][:service_dir], "etc","#{name}.conf") do
      source "#{name}.conf.erb"
      mode 0666
      owner node[:deployment][:user]
      group node[:deployment][:user]
    end
  end
#
  %w(startstone stopstone gslist).each do | name |
    template File.join(node[:gemstone][:service_dir], "bin", name) do
      source "#{name}.erb"
      mode 0500
      owner node[:deployment][:user]
      group node[:deployment][:user]
    end
  end
#
  %w(topaz).each do | name |
    template File.join(node[:gemstone][:service_dir], "bin", name) do
      source "#{name}.erb"
      mode 0555
      owner node[:deployment][:user]
      group node[:deployment][:user]
    end
  end
#
  template File.join(node[:gemstone][:service_dir], "bin", ".topazini") do
    source ".topazini.erb"
    mode 0444
    owner node[:deployment][:user]
    group node[:deployment][:user]
  end
#
  template File.join(node[:gemstone][:service_dir], "bin", "topaz_dc") do
    source "topaz_dc.erb"
    mode 0500
    owner node[:deployment][:user]
    group node[:deployment][:user]
  end
#
  %w(datacurator.tpz reset_passwords.tpz).each do | name |
    template File.join(node[:gemstone][:service_dir], "bin", name) do
      source "#{name}.erb"
      mode 0400
      owner node[:deployment][:user]
      group node[:deployment][:user]
    end
  end
#
  bash "Copy keyfile and extent" do
    user node[:deployment][:user]
    group node[:deployment][:user]
    cwd node[:gemstone][:service_dir]
    code <<-EOH
      cp #{node[:gemstone][:path]}/product/seaside/etc/gemstone.key etc
      cp #{node[:gemstone][:path]}/product/bin/extent0.dbf data
      chmod 644 data/extent0.dbf
      cp #{node[:gemstone][:path]}/product/data/system.conf etc
    EOH
    not_if do
      ::File.exists?(File.join(node[:gemstone][:service_dir], "data", \
        "extent0.dbf"))
    end
  end
#
  bash "Reset passwords" do
    user node[:deployment][:user]
    group node[:deployment][:user]
    cwd File.join(node[:gemstone][:service_dir], "bin")
    code <<-EOH
      ./startstone
      ./topaz < reset_passwords.tpz
      ./stopstone
    EOH
    not_if do
      ::File.exists?(File.join(node[:gemstone][:service_dir], "data", \
        "tranlog1.dbf"))
    end
  end
#
else
  Chef::Log.error("Installation of GemStone/S 64 Bit not supported 
     on this platform.")
end

Create recipes/gateway.rb with the following:

#
# Cookbook Name:: gateway
# Recipe:: default
#
# Copyright 2012, VMware
#
cloudfoundry_service "gemstone" do
  components ["gemstone_gateway"]
end

Create recipes/node.rb with the following:

#
# Cookbook Name:: node
# Recipe:: default
#
# Copyright 2012, VMware
#
cloudfoundry_service "gemstone" do
  components ["gemstone_node"]
end

Create templates/default/datacurator.tpz.erb to login into Topaz as DataCurator:

level 1
iferr 1 stk
iferr 2 stack
iferr 3 exit
set user DataCurator pass <%= node[:gemstone_node][:password] %>
set gems <%= node[:gemstone_node][:stonename] %>
login

Create templates/default/gemstone_gateway.yml.erb to configure the gateway:

---
cloud_controller_uri: <%= node[:cloud_controller][:service_api_uri] %>
service:
  name: gemstone
  version: "3.0"
  description: 'GemStone/S 64 Bit database service'
  plans: ['free']
  tags: ['gemstone', 'object']
host: localhost
index: <%= node[:gemstone_node][:index] %>
token: <%= node[:gemstone_node][:token] %>
mbus: nats://<%= \
     node[:nats_server][:user] %>:<%= \
     node[:nats_server][:password] %>@<%= \
     node[:nats_server][:host] %>:<%= \
     node[:nats_server][:port] %>/
pid: /var/vcap/sys/run/gemstone_service.pid
node_timeout: 2
logging:
  level: debug

Create templates/default/gemstone_node.yml.erb to configure the service node:

---
local_db: sqlite3:/var/vcap/services/gemstone/gemstone_node.db
mbus: nats://<%= \
     node[:nats_server][:user] %>:<%= \
     node[:nats_server][:password] %>@<%= \
     node[:nats_server][:host] %>:<%= \
     node[:nats_server][:port] %>/
index: <%= node[:gemstone_node][:index] %>
base_dir: /var/vcap/services/gemstone/
pid: /var/vcap/sys/run/gemstone_node.pid
available_memory: <%= node[:gemstone_node][:available_memory] %>
node_id: <%= "gemstone_node_#{node[:gemstone_node][:index]}" %>
max_memory: <%= node[:gemstone_node][:max_memory] %>
migration_nfs: /mnt/migration
logging:
  level: debug

Create templates/default/gs64stone.conf.erb for the stone configuration file:

KEYFILE = <%= node[:gemstone][:service_dir] %>/etc/gemstone.key;
DBF_EXTENT_NAMES = <%= node[:gemstone][:service_dir] %>/data/extent0.dbf;
STN_TRAN_LOG_DIRECTORIES = <%= node[:gemstone][:service_dir] %>/data/, 
     <%= node[:gemstone][:service_dir] %>/data/;
STN_TRAN_LOG_SIZES = 100, 100;
SHR_PAGE_CACHE_SIZE_KB = <%= node[:gemstone_node][:spc_size_kb] %>;
STN_TRAN_FULL_LOGGING = TRUE;

Create templates/default/gslist.erb to list the GemStone/S processes:

#!/bin/bash
export GEMSTONE=<%= node[:gemstone][:path] %>/product
export PATH=$GEMSTONE/bin:$PATH
export GEMSTONE_GLOBAL_DIR=<%= node[:gemstone][:service_dir] %>
gslist -cvl

Create templates/default/reset_passwords.tpz.erb change the passwords for the build-in users as part of the install process:

set gems <%= node[:gemstone_node][:stonename] %>
set user SystemUser pass swordfish
login
run
(AllUsers userWithId: 'SystemUser') 
     password: '<%= node[:gemstone_node][:password] %>'.
(AllUsers userWithId: 'DataCurator') 
     password: '<%= node[:gemstone_node][:password] %>'.
(AllUsers userWithId: 'GcUser') 
     password: '<%= node[:gemstone_node][:password] %>'.
(AllUsers userWithId: 'SymbolUser') 
     password: '<%= node[:gemstone_node][:password] %>'.
(AllUsers userWithId: 'Nameless') 
     password: '<%= node[:gemstone_node][:password] %>'.
System commitTransaction
%
logout
exit

Create templates/default/startstone.erb to start the GemStone/S database service:

#!/bin/bash
export GEMSTONE=<%= node[:gemstone][:path] %>/product
export PATH=$GEMSTONE/bin:$PATH
export GEMSTONE_GLOBAL_DIR=<%= node[:gemstone][:service_dir] %>
startstone -l <%= node[:gemstone][:service_dir] %>/log/gs64stone.log \
  -e <%= node[:gemstone][:service_dir] %>/etc/gs64stone.conf \
  -z <%= node[:gemstone][:service_dir] %>/etc/system.conf

Create templates/default/stopstone.erb to stop the GemStone/S database service:

#!/bin/bash
export GEMSTONE=<%= node[:gemstone][:path] %>/product
export PATH=$GEMSTONE/bin:$PATH
export GEMSTONE_GLOBAL_DIR=<%= node[:gemstone][:service_dir] %>
stopstone -i <%= node[:gemstone_node][:stonename] %> \
     DataCurator <%= node[:gemstone_node][:password] %>

Create templates/default/topaz.conf.erb as a configuration file for the Topaz processes:

GEM_TEMPOBJ_CACHE_SIZE = <%= node[:gemstone_node][:tempobj_cache_size] %>;

Create templates/default/topaz_dc.erb to start a Topaz process as DataCurator:

#!/bin/bash
export GEMSTONE=<%= node[:gemstone][:path] %>/product
export PATH=$GEMSTONE/bin:$PATH
export GEMSTONE_GLOBAL_DIR=<%= node[:gemstone][:service_dir] %>
topaz -l \
  -I <%= node[:gemstone][:service_dir] %>/bin/datacurator.tpz \
  -e <%= node[:gemstone][:service_dir] %>/etc/topaz.conf \
  -z <%= node[:gemstone][:service_dir] %>/etc/system.conf

Create templates/default/topaz.erb to start a non-DataCurator Topaz process:

#!/bin/bash
export GEMSTONE=<%= node[:gemstone][:path] %>/product
export PATH=$GEMSTONE/bin:$PATH
export GEMSTONE_GLOBAL_DIR=<%= node[:gemstone][:service_dir] %>
topaz -l \
  -I <%= node[:gemstone][:service_dir] %>/bin/.topazini \
  -e <%= node[:gemstone][:service_dir] %>/etc/topaz.conf \
  -z <%= node[:gemstone][:service_dir] %>/etc/system.conf

Create templates/default/.topazini.erb to provide a general Topaz initialization:

level 1
iferr 1 stk
iferr 2 stack
iferr 3 exit
set gems <%= node[:gemstone_node][:stonename] %>

When these files have been created, you can rerun the deployment script:

~/cloudfoundry/vcap/dev_setup/bin/vcap_dev_setup -d ~/cloudfoundry

This will download and install the GemStone/S product at ~/cloudfoundry/.deployments/devbox/deploy/gemstone/product. It will also create a place to hold the database at:

  • /var/vcap/services/gemstone/
    • bin/
      • datacurator.tpz
      • gslist
      • reset_passwords.tpz
      • startstone
      • stopstone
      • topaz
      • topaz_dc
      • .topazini
    • data/
      • extent0.dbf
    • etc/
      • gemstone.key
      • gs64stone.conf
      • system.conf
      • topaz.conf
    • locks/
    • log/

From the bin directory you can start the database and login as DataCurator and execute various Topaz commands.

Later we will look at presenting GemStone/S 64 Bit as a service in Cloud Foundry.

We looked at deploying a trivial Smalltalk application to Cloud Foundry (here) and we looked at creating a Smalltalk application that uses MySQL for persistence (here). Now we combine the two so that we use MySQL in a Smalltalk application in Cloud Foundry.

Changes on the Client

Cloud Foundry has MySQL 5.1 built-in, and connection information is provided as a JSON document in an environment variable (VCAP_SERVICES). Our sample application already gets MySQL configuration information from this environment variable, so no change is needed to the Smalltalk code. We do, however, have a slight problem with the startup script. On the client we have set up a development directory as follows:

  • ~/cloud/aida/
    • Aida.image
    • Aida.changes
    • PharoV10.sources
    • start
    • app/
      • aida.st
      • *.mcz

When testing on the client we start the application using a shell script, ~/cloud/aida/start, and it launches Pharo and executes the Smalltalk code found in app/aida.st. We did this to isolate the “application” (the things that need to be pushed to Cloud Foundry) and keep it as small as possible. By contrast, the deployment directory on Cloud Foundry is structured as follows:

  • /var/vcap.loca/dea/apps/<name>-<instance #>-<random hex number>/
    • run.pid
    • startup (build by CF)
    • stop (build by CF)
    • logs/
    • app/
      • Aida.image (copied from /opt/smalltalk/aida/ on the server by CF)
      • Aida.changes (copied from /opt/smalltalk/aida/ on the serverby CF)
      • PharoV10.sources (copied from /opt/smalltalk/aida/ on the serverby CF)
      • main.st (built by CF)
      • staging.st (built by CF)
      • aida.st (copied from the client)
      • *.mcz (copied from the client)

Note that on the client the image is in the parent of aida.st while on the server the image is in the same directory as aida.st. This affects the default directory visible to Smalltalk and at present it is different on the client and on the server. Until now, this was not an issue, but our MySQL demo installs MCZ files from the app directory and the aida.st script we provided here includes lines like the following:

MczInstaller installFileNamed: 'app/BalanceDemo-JamesFoster.1.mcz'.

On the client this works fine where the default is the parent of app/, but on the server where the default is app/ this fails because it can’t find the file to install. Our goal, of course, is to have a client environment that is as close to the server as possible so that debugging is easier. We will fix this by modifying the client so that it changes the default directory to app/ during the startup process. On the client we have a bash script that sets up environment variables to simulate the server and then launch CogVM with a particular image and startup script. Previously we pointed to app/aida.st as the startup script. We will change our Bash startup script, start, so that we use a different Smalltalk startup script, start.st:

#! /bin/bash 
export VCAP_SERVICES='{
  "mysql-5.1":[ {
    "credentials":{
      "name":"test",
      "host":"localhost",
      "port":3306,
      "user":"jfoster",
      "password":"swordfish"
    }
  }
]}'
export VCAP_APP_PORT=8888
open -a /opt/smalltalk/cog/CogVM.app/ --args \
  ~/cloud/aida/Aida.image \
  ~/cloud/aida/start.st \
$VCAP_APP_PORT

Now we create the Smalltalk script:

| file contents |
FileDirectory setDefaultDirectory:
  (FileDirectory default entryAt: 'app')
  asFileDirectory fullName.
file := FileStream readOnlyFileNamed: 'aida.st'.
contents := file contentsOfEntireFile.
Compiler evaluate: contents.

This script changes the default directory and then evaluates aida.st (which is now in the default directory). We then modify aida.st so that it assumes that it is in the default directory by removing the ‘app/‘ portion of the file path:

Author fullName: 'CloudFoundry'. "Name for edits"
MczInstaller installFileNamed: 'JSON-ul.35.mcz'.
MczInstaller installFileNamed: 'Stdb-Core-ah.7.mcz'.
MczInstaller installFileNamed: 'Stdb-MysqlProtocol-ah.15.mcz'.
MczInstaller installFileNamed: 'Stdb-MysqlImpl-ah.9.mcz'.
MczInstaller installFileNamed: 'BalanceDemo-JamesFoster.1.mcz'.
MySqlBalanceApp register.
ImageBalanceApp register.

We can test the application by starting it on the client:

~/cloud/aida/start

We can navigate to http://localhost:8888/MySqlBalance and see that the application works.

Deploying to the Server

We can then try deploying the application to our private Cloud Foundry that is configured to accept Smalltalk applications (created here). On the client enter ‘vmc push‘ in a shell from the app directory:

pitcairn:app jfoster$ vmc push
Would you like to deploy from the current directory? [Yn]: 
Application Name: balance
Application Deployed URL [balance.vcap.me]: 
Detected a AidaWeb Application, is this correct? [Yn]: 
Memory reservation (128M, 256M, 512M, 1G, 2G) [128M]: 
How many instances? [1]: 
Create services to bind to 'balance'? [yN]: y
1: mongodb
2: mysql
3: neo4j
4: redis
What kind of service?: 2
Specify the name of the service [mysql-29b9]: balance-data
Create another? [yN]: 
Would you like to save this configuration? [yN]: 
Creating Application: OK
Creating Service [balance-data]: OK
Binding Service [balance-data]: OK
Uploading Application:
 Checking for available resources: OK
 Processing resources: OK
 Packing application: OK
 Uploading (0K): OK 
Push Status: OK
Staging Application 'balance': ..............................

In response to the questions, accept the defaults with the following exceptions: name the application ‘balance’; ask to create a service (y); select mysql (2) as the service; and name the service something meaningful (e.g., ‘balance-data’). Depending on the speed of your cloud, you might get a timeout during the staging or it might report that the application failed to start. In my experience things started and we can see that the balance app is using the balance-data service:

pitcairn:app jfoster$ vmc apps
+-------------+----+---------+-----------------+--------------+
| Application |  # | Health  |       URLS      |   Services   |
+-------------+----+---------+-----------------+--------------+
|   balance   |  1 | RUNNING | balance.vcap.me | balance-data |
+-------------+----+---------+-----------------+--------------+
pitcairn:app jfoster$ vmc services
============== System Services ==============
+---------+---------+-------------------------------+
| Service | Version |          Description          |
+---------+---------+-------------------------------+
| mongodb |   1.8   |      MongoDB NoSQL store      |
|  mysql  |   5.1   |    MySQL database service     |
|  neo4j  |   1.4   |       Neo4j NOSQL store       |
|  redis  |   2.2   | Redis key-value store service |
+---------+---------+-------------------------------+
=========== Provisioned Services ============
+--------------+---------+
|     Name     | Service |
+--------------+---------+
| balance-data |  mysql  |
+--------------+---------+
pitcairn:app jfoster$

Conclusion

We have deployed a Smalltalk (Aida/Cog) application that uses MySQL to Cloud Foundry. In this case we deployed only one instance because there is still some state being saved in the image and one instance is a trivial way to achieve session affinity. It seems that we cannot get to the balance application on Aida without logging in (using the default admin/password). When we have multiple instances, the web requests can go to any instance and the image that presented the login page might not be the same image that gets the login attempt. Of course, this problem would exist even more with (non-GemStone) Seaside, where session state is stored in the image and a redirect is even done between the callback and rendering.

In an earlier discussion about Smalltalk we observed that image-based persistence does not work very well with Cloud Foundry’s model where several application instances may be run at the same time to support scaling. With a Pharo-based Smalltalk, each instance has its own image so data stored in the image is not shared among instances. One way to solve that problem is to use an external database, and Cloud Foundry includes built-in support for MySQL 5.1. In this post we look at adding MySQL to the client and creating an AidaWeb application that uses the database. Deploying this application on Cloud Foundry will require a few modifications that we will discuss in the next post.

MySQL Setup

We start by adding MySQL to our client so we can develop and test our application locally. On this page, select the appropriate download for your machine; I used Mac OS X 10.6 (AMD64, installer format) (13 Feb 2011, 78.1M). Follow the instructions for installing and starting MySQL. For convenience, we will add an alias to the MySQL command-line interface:

alias mysql="/user/local/mysql/bin/mysql"

It is a good practice to secure the database, so from a command shell we connect to the database:

mysql -u root

Once connected, we add a password for the root user:

SELECT User, Host, Password from mysql.user;
UPDATE mysql.user 
  SET Password = PASSWORD('swordfish') 
  WHERE User = 'root';
FLUSH PRIVILEGES;
SELECT User, Host, Password from mysql.user;
EXIT

Now, from the command shell try various ways of connecting (note that there is no space after the -p option:

mysql -u root
mysql -u root -p
mysql -u root -pswordfish

You should find that the first one fails, the second one prompts for a password, and the third one logs in immediately. Once logged in we can perform various queries. Note that we need to specify a database as part of the query if it is not explicitly used. Try each of the following individually:

SELECT User FROM user;
SELECT User FROM mysql.user;
USE mysql; SELECT User FROM user;

The first should result in an error while the next two should succeed. You can avoid the USE command by specifying the database as a command-line argument (exit from MySQL and try the following):

mysql -u root -pswordfish mysql

Next we create a non-root administrative user:

CREATE USER 'jfoster'@'localhost' IDENTIFIED BY 'swordfish';
GRANT ALL PRIVILEGES ON *.* TO 'jfoster'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT

Now we can login as our new user and specify the test database:

mysql -u jfoster -pswordfish test

The MySQL driver that we will use has a couple hundred tests that expect a particular user to be defined. We enter the following to create a new user:

CREATE USER 'stdbtestuser'@'localhost' IDENTIFIED BY 'stdbtestpass';
GRANT ALL PRIVILEGES ON stdbtestschema.* TO 'stdtestuser'@'localhost';
FLUSH PRIVILEGES;

Creating a Smalltalk Application

Now that we have MySQL running on our client machine, we will create a Smalltalk application that uses MySQL. (In creating this AidaWeb application I got help from Janko Mivšek.) Open a web browser on http://ss3.gemstone.com/ss/CloudFoundryDemos.html and click on the ‘Latest’ tab. There should be several packages listed. For this demo we want the following:

  • JSON
  • Stdb-Core
  • Stdb-MysqlProtocol
  • Stdb-MysqlImpl
  • BalanceDemo

At the right end of each row is a “download” action; for each of the above packages click this link. Move the downloaded files from the Downloads directory to ~/cloud/aida/app (which we created earlier).

Now we edit ~/cloud/aida/app/aida.st to install the packages for our application and register two entry points with Aida:

Author fullName: 'CloudFoundry'. "Name for edits"
MczInstaller installFileNamed: 'app/JSON-ul.35.mcz'.
MczInstaller installFileNamed: 'app/Stdb-Core-ah.7.mcz'.
MczInstaller installFileNamed: 'app/Stdb-MysqlProtocol-ah.15.mcz'.
MczInstaller installFileNamed: 'app/Stdb-MysqlImpl-ah.9.mcz'.
MczInstaller installFileNamed: 'app/BalanceDemo-JamesFoster.1.mcz'.
MySqlBalanceApp register.
ImageBalanceApp register.

Recall that this script runs once, during the “staging” process, and then the image is saved and made read-only so it can be deployed in multiple instances. Now we edit ~/cloud/aida/start so that it defines environment variables that will be provided by Cloud Foundry:

#! /bin/bash 
export VCAP_SERVICES='{
  "mysql-5.1":[ { 
    "credentials":{ 
      "name":"test", 
      "host":"localhost", 
      "port":3306, 
      "user":"jfoster", 
      "password":"swordfish" 
    } 
  }
]}'
export VCAP_APP_PORT=8888
open -a /opt/smalltalk/cog/CogVM.app/ --args \ 
  ~/cloud/aida/Aida.image \ 
  ~/cloud/aida/app/aida.st \ 
  $VCAP_APP_PORT

Now, try launching the application locally:

~/cloud/aida/start

It should take some time loading the packages and then you should be able to open a Test Runner and run the JSON and Stdb-Mysql* tests. You should have 223 tests all pass. You can now open a System Browser and look at the BalanceDemo category (it should be the last one in the first column). You can see that there is a BalanceApp, an abstract superclass that provides the AidaWeb behavior, and two subclasses that implement image-based persistence and persistence using MySQL. Browse the code and then open a web browser on http://localhost:8888/ImageBalance and http://localhost:8888/MySqlBalance. (The first time you may need to login to AidaWeb using the user ‘admin’ and the password ‘password’.) With each of them you should be able to enter a value in the ‘Adjustment:’ field, then click the ‘Adjust’ button, and see a new balance. Try entering both positive and negative integers.

From a MySQL command-line interface try the following lines and look at the change in the application by refreshing the web page:

SHOW TABLES;
SELECT * FROM account;
UPDATE account SET balance = 42;

We can see that changes from the web are reflected in the database and changes in the database are reflected in the web.

Cloud Foundry is an Platform as a Service (PaaS) system that includes a number of popular languages and web frameworks. Because it is open it is possible to add other runtimes and frameworks. In this screencast we describe the process of preparing Smalltalk (AidaWeb on CogVM) to be deployed to the cloud and the changes made to Cloud Foundry to host Smalltalk. We follow the steps described here.

Cloud Foundry comes with built-in support for a number of web frameworks and runtimes, but one of the features of being “open” is that we have access to the source code and can modify it to support new frameworks and runtimes. In this video (7:20) we follow the steps here to add support for Perl. We create a trivial Perl application and deploy it to our private cloud, demonstrating the extensibility of Cloud Foundry.


This video (6:53) is based on the blog post here and demonstrates the process for creating a private installation of Cloud Foundry. Having a private cloud is helpful for debugging before deploying on a public cloud. It is also necessary if we want to add a new runtime or framework to Cloud Foundry. We start by creating a new virtual machine in VMware Fusion with Ubuntu 10.04, add a few necessary packages (including Java!), then install Cloud Foundry. Then from the client we “push” a trivial Ruby Sinatra application and observe that two instances of the application are deployed on the server. We will use this private cloud for subsequent projects.

When I last followed the instructions for creating a private cloud, the vcap_dev_setup script completed without errors. Today when I tried it I got an error:

Chef::Exceptions::Package: No version specified, 
and no candidate version available for sun-java6-bin

The package is required in dev_setup/cookbooks/java/recipes/default.rb:

bash "Setup java" do
  code <<-EOH
  add-apt-repository "deb http://archive.canonical.com/ lucid partner"
  apt-get -qqy update
  echo sun-java6-jdk shared/accepted-sun-dlj-v1-1 boolean true | /usr/bin/debconf-set-selections
  echo sun-java6-jre shared/accepted-sun-dlj-v1-1 boolean true | /usr/bin/debconf-set-selections
  EOH
  not_if do
    ::File.exists?("/usr/bin/java")
  end
end
%w[ curl sun-java6-bin sun-java6-jre sun-java6-jdk].each do |pkg|
  package pkg do
    not_if do
      ::File.exists?("/usr/bin/java")
    end
  end
end

It seems that on 2012-02-17 this package was deleted from the lucid partner repository, so the above script no longer works. The explanation is that “Oracle (Sun) Java 6 is no longer available to be distributed by Ubuntu, because of license issues.”

Note that the above code will not attempt to install Java if it is already present, so the error can be avoided by installing Java before starting the vcap_dev_setup script. One approach, suggested at superuser.com, is to get the packages from another repository:

sudo apt-get install python-software-properties
sudo add-apt-repository ppa:ferramroberto/java
sudo apt-get update
sudo apt-get install sun-java6-bin sun-java6-jre sun-java6-jdk

You will be required to accept the license. After it finishes, you can confirm that this does indeed install things:

dpkg --get-selections | grep java

Also you can see that the executable exists:

ll /usr/bin/java

There certainly are other ways to add Java to Ubuntu and I don’t suggest that this is the best way–only that it worked for me and now the vcap_dev_setup script completes successfully!

Follow

Get every new post delivered to your Inbox.