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.