21 October 2014

Kinokero (cloudprint) tutorial part 2

Kinokero Tutorial Part 2: Cloudprinting

Using Google Cloudprint Proxy Connector in Ruby


Kinokero is a ruby gem which provides a complete Google CloudPrint Proxy Connector functionality. That means that it can turn any OS-connected printer into a cloudprint accessible printer, as long as the internet connection is maintained.

This post is a continuation of Part 1 Getting Started, so please check that out for prerequisites and expected environments.

A basis for building a proxy connector

Within the gem source itself, there are two places that demonstrate using kinokero: the console and the testing setup helper. The console is located at: console/twiga.rb, and the testing helper is located at: test/test/test_kinokero.rb. Because testing is kind of specific and different, using the console as a template would be better.

Console

During development, I needed a convenient setup and testing structure to individually trigger GCP primitives and see traces of the request and result. I'm calling that the "console" and it has no function inherent to the gem, other than a convenient setup and debugging apparatus. Rather than having it vanish, I have made it part of the gem superstructure and it can be run independently, as though it were an application, for calling and testing the gem.

The console has a simple means of persistence (a seed yaml file) for any printers which are registered. The seed yaml requires, at the least for initial startup, a section of data for the (required) test printer.

In this tutorial, I will try to point out the important aspects to keep when building a program to use the kinokero proxy connector.

Structural

I use both rvm (ruby version manager) together with bundler to manage my various project environments. So you'll need a Gemfile with at least the following line:
  gem 'kinokero'

then you'll need to run the bundler:
  $ bundle install

Configuration and initialization

Next, think about any configuration and initialization you'll need. The console does it in the following way, which should go at the head of your program:

main module invokes configuration
#!/usr/bin/env ruby

# *************************************************************************
# *****  app configured, initialized, and ruby-prepped here   *************
# *************************************************************************

    # make it so that our lib is at the head of the load/require path array
  $:.unshift( File.expand_path('../lib', __FILE__) )
    # kick off module configurations
  load File.expand_path('../config/application_configuration.rb', __FILE__)
  
# *************************************************************************

  require 'yaml'
  require 'erb'
  require 'active_support/core_ext/hash'

application_configuration.rb hold any basic gem invocation stuff

And config/application_configuration.rb basically is the following (of course you'll need to change to your own namespace conventions):
# *************************************************************************
# *******    general configuration for console appliance    ***************
# *************************************************************************

module Twiga

# *************************************************************************
require 'rubygems'
require "kinokero"
require 'json'

# *************************************************************************
# ******  mimic the way RAILS sets up required gems  **********************
# Set up gems listed in the Gemfile.
# *************************************************************************
# 
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])

# *************************************************************************

# ########################################################################
end  # module Twiga

Retrieve Persistence

Your main module will need to retrieve any existing printer information from persistence. The console uses config/gcp_seed.yml for its persistence (it needs to be in writable medium). The following routines are your basic read/write for the persistence. Also included is build_device_list which will return a kinokero-style gcp_control_hash from the seed data.

# *************************************************************************
# get managed printer data
# *************************************************************************

GCP_SEED_FILE = "../config/gcp_seed.yml"

# #########################################################################
# ##########  working with seed data  #####################################
# #########################################################################

# -------------------------------------------------------------------------

  def load_gcp_seed()
    if @seed_data.nil?
      @seed_data =  YAML.load( 
          ERB.new(
            File.read(
              File.expand_path(GCP_SEED_FILE , __FILE__ ) 
            )
          ).result 
      )

    end
  end

# ------------------------------------------------------------------------

  def write_gcp_seed()
    unless @seed_data.nil?

      File.open(
        File.expand_path(GCP_SEED_FILE , __FILE__ ), 
        'w'
      ) { |f| 
        YAML.dump(@seed_data, f) 
      }

    end   # unless no seed data yet
  end

# ------------------------------------------------------------------------

  def build_device_list()

    load_gcp_seed()    # load the seed data

      # prep to build up a hash of gcp_control hashes
    gcp_control_hash = {}

    @seed_data.each_key do |item|
      
         # strip item into hash with keys
      gcp_control_hash[ item ] = @seed_data[ item ].symbolize_keys

    end   # convert each seed to a device object

    return gcp_control_hash
    
  end   # convert each seed to a device object

# ------------------------------------------------------------------------


Command line options

If you wish to have some command line switches to control kinokero, the following function will return an options hash from the command line switches.

# #######################################################################
# ##########  command line options handling  ############################
# #######################################################################

# -----------------------------------------------------------------------

  def parse_options()

    options = {}

    ARGV.each_index do |index|
      case $*[index]
        when '-m' then options[:auto_connect] = false
        when '-v' then options[:verbose] = true
        when '-q' then options[:verbose] = false
        when '-t' then options[:log_truncate] = true
        when '-r' then options[:log_response] = false
      else
        ::Twiga.say_warn "unknown option: #{arg}"
      end    # case

      $*.delete_at(index)   # remove from command line

    end   # do each cmd line arg
      
    return Kinokero::Cloudprint::DEFAULT_OPTIONS.merge(options)

  end

# ------------------------------------------------------------------------

Invocation & instantiating a Proxy object

The console just jumps into the following code as part of the main module. Note that build_device_list automatically loads and parses the seed yml file, if it hasn't already been loaded. As kinokero processes this device list, it will automatically setup and activate any logical cloudprint printer which is designated as active and it will establish an on-line connection with GCPS unless the :auto_connect option is false.

     # start up the GCP proxy
  @proxy = Kinokero::Proxy.new( build_device_list(), parse_options )

    # remember the device list for easy access
  @my_devices = @proxy.my_devices   # not necessary to extract locally

Register & remove a printer

These are the most basic two actions which a Proxy is expected to perform. To update persistence, the console has several helper methods: update_gcp_seed, add_gcp_seed_request, and write_gcp_seed. These can be found in the console and are not included here, as they are not germane to the main usage of kinokero.


# ------------------------------------------------------------------------

  def do_register( item )
    new_request = build_gcp_request( item )

    response = @proxy.do_register( new_request ) do |gcp_control|

      update_gcp_seed(gcp_control, gcp_control[:item] ) do |seed|
        add_gcp_seed_request( seed, new_request )
      end  # seed additions

    end   # do persist new printer information

    unless response[:success]
      puts "printer registration failed: #{response[:message]}"
    end

  end


# ------------------------------------------------------------------------

  def do_delete( item )
    item = validate_item( item )
    @proxy.do_delete( item )
    @seed_data[item]['is_active'] = false
    write_gcp_seed()
  end

# ------------------------------------------------------------------------


# if item hasn't yet been defined in seed data, create one out of
# thin air by using test as a template
# ------------------------------------------------------------------------

  def build_gcp_request( item )

    use_item = validate_item( item )

    return {
      item:  item,
      printer_id:   0,  # will be cue to create new record
      gcp_printer_name: "gcp_#{item}_printer",
      capability_ppd: @seed_data[use_item]['capability_ppd'],
      capability_cdd: @seed_data[use_item]['capability_cdd'],
      cups_alias: @seed_data[use_item]['cups_alias'],
      gcp_uuid:         @seed_data[use_item]['gcp_uuid'],
      gcp_manufacturer: @seed_data[use_item]['gcp_manufacturer'],
      gcp_model:        @seed_data[use_item]['gcp_model'],
      gcp_setup_url:    @seed_data[use_item]['gcp_setup_url'],
      gcp_support_url:  @seed_data[use_item]['gcp_support_url'],
      gcp_update_url:   @seed_data[use_item]['gcp_update_url'],
      gcp_firmware:     @seed_data[use_item]['gcp_firmware'],
    }
  end

# ------------------------------------------------------------------------

  def validate_item( item )
    return ( @seed_data.has_key?(item) ? item : 'test' )
  end

# ------------------------------------------------------------------------

Putting it all together

All the above sections are part of the main module in the console, console/twiga.rb.  So you can put these together in any manner that makes sense to your purpose.  Most of the methods shown above are merely helper functions to prep the required kinokero data structures.

17 October 2014

Kinokero (cloudprint) tutorial part 1

Kinokero Tutorial Part 1: Getting Started

Using Google Cloudprint Proxy Connector in Ruby

Kinokero is a ruby gem which provides a complete Google CloudPrint Proxy Connector functionality. That means that it can turn any OS-connected printer into a cloudprint accessible printer, as long as the internet connection is maintained.

The gem itself includes separate classes to handle the GCP server protocol (Cloudprint), the GCP Jingle notification protocol (Jingle), and a class for interacting with CUPS devices on a linux system (Printer). Persistence is expected to be handled by whatever is invoking Kinokero. The initial beta release of kinokero is working for CUPS-style printer on linux-like OSs.

Parts 1 and 2 of this tutorial will show how to get kinokero up and running in a ruby application, focusing only on the highest Proxy-level, and will not show how to interact directly with the lower-level Google Cloudprint Services (GCPS), such as takes place in the Class Cloudprint.

The kinokero gem itself has a small working code to invoke the gem. It is contained in the console folder and represents the best template for getting the gem working. This tutorial will cover the preparation (Part 1) and the key points of invoking the gem (Part 2).

Note: I use Ruby 2.0 in an RVM environment on an Ubuntu (linux) workstation, so this tutorial will be specific to that type of environment. You will also want to make sure you have registered, with CUPS, a working printer on your *nix system.

Registering with Google APIs; setting environment variables

You'll need a client ID for your proxy for obtaining OAuth2 authorization codes, as the GCP documentation points out:
The client ID can be obtained as explained  here ». [relevant portion shown below] Client IDs don't need to be unique per printer: in fact, we expect one client ID per printer manufacturer.

Before your application can use Google's OAuth 2.0 authentication system for user login, you must set up a project in the Google Developers Console to obtain OAuth 2.0 credentials, set a redirect URI, and (optionally) customize the branding information that your users see on the user-consent screen. You can also use the Developers Console to create a service account, enable billing, set up filtering, and do other tasks. For more details, see the Google Developers Console Help.
Specifically, you'll end up with four items that you'll need to put into the environmental variables accessible by your application. This is done as environmental variables for security concerns, so that the values won't appear in any public repositories for either the gem or your application. See below for names and sample data. I put these into my .bashrc file. This only needs to be done once for no matter how many proxy connectors you wish to invoke.

  export GCP_PROXY_API_PROJECT_NBR=407407407407
  export GCP_PROXY_API_CLIENT_EMAIL="407407407407@developer.gserviceaccount.com"
  export GCP_PROXY_CLIENT_ID="407407407407-abcd1abcd2abcd3abcd4abcd5abcd5ef.apps.googleusercontent.com"
  export GCP_PROXY_CLIENT_SECRET="someSECRETencryptedValue"

The CLIENT_SECRET will be a typical encrypted gibberish.

On a personal note, using the Google Developers Console to get these values was not straightforward. So you may have trial & error wrong turns as you go about trying to coax these values out of the Great And Wonderful Wizard of Oz.

Adding a resource for persistence

The Kinokero proxy requires the invoking code to provide persistence for GCPS-issued information (such OAUTH2 tokens, printer id, etc). One of the reasons for this is when (or unexpectedly if) the entire machine running the proxy is restarted, the cloudprint printers, which had already been registered, need to be brought on-line with GCPS. This is accomplished through persistence of the critical information.

The console itself relies on a yaml file for persistence ( console/config/gcp_seed.yml ).

Kinokero has a primary hash which is required by several of the classes within the gem. In the README, this is called 'gcp_control' hash, and is discussed in detail in that document. The yaml seed file is used to prep this hash prior to instantiating a Proxy object.

Adding seed data for a test printer

To set up the seed data for your test printer, go to your OS and discover the name of an attached printer.
  $ lpstat -v

device for laserjet_1102w: hp:/net/HP_LaserJet_Professional_P_1102w?ip=192.168.1.65
device for lp_null: ///dev/null

In the sample shown above, there are two printers registered: "laserjet_1102w" and "lp_null." The former is the only working actual printer, so this would be chosen to be the test printer. We'll next need to discovery the full path to the PPD file for that printer. On an Ubuntu system, it will be: /etc/cups/ppd/laserjet_1102w.ppd . You will need to convert this into the gcp v2.0 required CDD format. Google has a handy converter that makes that easy. You can access it here ». Once you've converted the file, name it and place it in: /etc/cups/cdd/laserjet_1102w.cdd .

You'll also need information about the actual device: manufacturer, model, firmware version number, serial number for the printer (aka uuid), and some URLs for setup, support, and updates for the printer. I'm not sure how GCPS uses these, if at all, at this time.

Armed with this information, edit console/config/gcp_seed.yml, obviously replacing with your actual values in the appropriate places. The following attributes are required prior to registering the test printer.
  item: 'test',
  cups_alias: 'laserjet_1102w',
  gcp_printer_name: 'gcp_test_printer',
  capability_ppd: '/etc/cups/ppd/laserjet_1102w.ppd',
  capability_cdd: '/etc/cups/cdd/laserjet_1102w.cdd',
  gcp_uuid: 'VND3R11877',
  gcp_manufacturer: 'Hewlett-Packard',
  gcp_model: 'LaserJet P1102w',
  gcp_setup_url: 'http://www8.hp.com/us/en/campaigns/wireless-printing-center/printer-setup-help.html',
  gcp_support_url: 'http://h10025.www1.hp.com/ewfrf/wc/product?product=4110396&lc=en&cc=us&dlc=en&lang=en&cc=us',
  gcp_update_url: 'http://h10025.www1.hp.com/ewfrf/wc/product?product=4110396&lc=en&cc=us&dlc=en&lang=en&cc=us',
  gcp_firmware: '20130703'

In Part 2, we'll show how to instantiate a kinokero proxy object and register a printer.