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
- gemstone_service/
- resources/
- provision.tpz.erb
- unprovision.tpz.erb
- Gemfile
- Gemfile.lock
- Rakefile
- README
- .gitignore
- bin/
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.
1 comment
Comments feed for this article
March 13, 2012 at 6:23 pm
Adding Topaz as a Runtime in Cloud Foundry « Programming Gems (on GemStone)
[…] our last post we walked through adding GemStone/S as a service in Cloud Foundry. With this, we could access […]