From 34603a617b19d271230a702a7816e3a0a4c2bb18 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 28 May 2019 17:41:57 +0100 Subject: [PATCH 1/4] (FM-8079) Resource API and Transports Hands-on-Lab --- docs/README.md | 7 + docs/hands-on-lab/01-installing-prereqs.md | 16 ++ ...-connecting-to-the-lightbulbs-emulator.png | Bin 0 -> 43292 bytes .../02-connecting-to-the-lightbulbs.md | 28 +++ docs/hands-on-lab/03-creating-a-new-module.md | 85 ++++++++ .../03-creating-a-new-module_vscode.png | Bin 0 -> 22263 bytes .../hands-on-lab/04-adding-a-new-transport.md | 105 +++++++++ .../04-adding-a-new-transport/.sync.yml | 9 + .../lib/puppet/transport/hue.rb | 36 ++++ .../lib/puppet/transport/schema/hue.rb | 28 +++ .../puppet/util/network_device/hue/device.rb | 15 ++ .../spec/unit/puppet/transport/hue_spec.rb | 56 +++++ .../unit/puppet/transport/schema/hue_spec.rb | 8 + .../05-implementing-the-transport-hints.md | 19 ++ .../05-implementing-the-transport.md | 126 +++++++++++ .../06-implementing-the-provider.md | 201 ++++++++++++++++++ docs/hands-on-lab/07-implementing-a-task.md | 181 ++++++++++++++++ 17 files changed, 920 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/hands-on-lab/01-installing-prereqs.md create mode 100644 docs/hands-on-lab/02-connecting-to-the-lightbulbs-emulator.png create mode 100644 docs/hands-on-lab/02-connecting-to-the-lightbulbs.md create mode 100644 docs/hands-on-lab/03-creating-a-new-module.md create mode 100644 docs/hands-on-lab/03-creating-a-new-module_vscode.png create mode 100644 docs/hands-on-lab/04-adding-a-new-transport.md create mode 100644 docs/hands-on-lab/04-adding-a-new-transport/.sync.yml create mode 100644 docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/hue.rb create mode 100644 docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/schema/hue.rb create mode 100644 docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/util/network_device/hue/device.rb create mode 100644 docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/hue_spec.rb create mode 100644 docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/schema/hue_spec.rb create mode 100644 docs/hands-on-lab/05-implementing-the-transport-hints.md create mode 100644 docs/hands-on-lab/05-implementing-the-transport.md create mode 100644 docs/hands-on-lab/06-implementing-the-provider.md create mode 100644 docs/hands-on-lab/07-implementing-a-task.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..1a30afb1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Resource API hands-on lab + +This lab will walk you through the basic steps of creating a native integration with puppet. After this you will have a fully functioning module to manage Philips HUE lights and have seen all the bits fall into place. + +These labs are intended for new and experienced developers alike. Please post feedback and suggestions for improvement in the [issues section](https://github.com/puppetlabs/puppet-resource_api/issues). + +Start with [installing the Puppet Development Kit](./hands-on-lab/01-installing-prereqs.md) diff --git a/docs/hands-on-lab/01-installing-prereqs.md b/docs/hands-on-lab/01-installing-prereqs.md new file mode 100644 index 00000000..b4aae380 --- /dev/null +++ b/docs/hands-on-lab/01-installing-prereqs.md @@ -0,0 +1,16 @@ +# Install the Puppet Development Kit and other tools + +To start out, install the Puppet Development Kit (PDK), which will provide all necessary tools and libraries to build and test modules. Additionally we recommend an emulator for the target device of this lab, a code editor with good ruby and puppet support, and GIT, a version control system to keep track of our progress. + +1. Choose your platform from https://puppet.com/download-puppet-development-kit; download and install the package + +2. If you do not have a Philips HUE hub available, you can download the [Hue-Emulator](https://github.com/SteveyO/Hue-Emulator/raw/master/HueEmulator-v0.8.jar). You will need to have Java installed to run this. + +3. To edit code, we recommend the cross-platform editor [VSCode](https://code.visualstudio.com/download), with the [Ruby](https://marketplace.visualstudio.com/items?itemName=rebornix.Ruby) and [Puppet](https://marketplace.visualstudio.com/items?itemName=jpogran.puppet-vscode) extensions. There are lots of other extensions that can help you with your development workflow. + +4. Git is a version control system that helps you keep track of changes and collaborate with others. In the course of the hands-on lab, we will show some integrations with cloud services that can help you. If you never used git before, skip this and all related steps for now. + + +## Next up + +Having installed all of this, let's [light up a few](./02-connecting-to-the-lightbulbs.md). diff --git a/docs/hands-on-lab/02-connecting-to-the-lightbulbs-emulator.png b/docs/hands-on-lab/02-connecting-to-the-lightbulbs-emulator.png new file mode 100644 index 0000000000000000000000000000000000000000..57b3c59feb013ac2377b7039b46007b0a751e409 GIT binary patch literal 43292 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 z z)%Y6xQTf)_xw+8nRr(q%Iw$K3a8zq?&&xz+4WfNPw6r;Ak2Hg3G3!DWzIz`a?(9xs zLqd$U$zg(dj~*1}g-QHw$?v|E{V`wut=IOl6EwfY_{%$Id=o2o&7UPllz}!2pPeLP zpM5TgXj!3ker&}0lTR6SZlb5wFx@GMD!QNh*7k9e{cKspZPJ#=c* zE?ywZ^*^PezPt?J;;Mi(HjNgNHa`VQXbX<>#NR;{QHLj=L>HhhA+=txhL~MF@+Nic zOG9Mf6#tz1g01U*EE;|YfT=b_sMq#BN6BX3-g901{%O_*++Ii?k#Sc<;qvV4OUA|x z_wP$F-0454RZ)o=z5%cLL#YAyH0b7s)@c2guion!s}NYkfyo{oZ?Z6#X(5I4F`;CZ zP-yVjGkw}^NhSv$zq-A!POD_L`>dGY{;Ek)Wq)_f5S z4gL6WD$W^UBo*T7H_4MxO7R=Dp5c8E>+U2^KCgl$%&Q*>t|}nw9IM<VL%eLsXJ(s|29|^&{(WebDpP74=Ef&45=i=g$ zyxj3!DRyVv3!rKds`-<1n+eN3N4DkoNXtWS{4_-2F1;*d06+i$YK7%v?bd{*p_t!* z7zeY+4F;rERzL%cs_zNl?Av0`Im!?^)9Quki_6M!lR3kw^OIict}0YEgurXsus zON{UhIiZfLf3v3kxu_z>x|X!zw4$UU+zPHFOI(>wS@BYpZm%w#Rw82%O^$rMAE^9M zjE2J~K5MVOzJ7N%oc)flhzPN=Qbi@x@s7Z0aWyq(true} + Evaluated 6 tests in 2.405066937 seconds: 0 failures, 0 pending. +david@davids:~/tmp/hue$ +``` + +If you're working with a version control system, this would also be a good point to make the first commit to store away all the boilerplate code and later revisit the changes you made. Here I'm showing how to initialise a local git repository and storing all files in an initial commit: + +``` +david@davids:~/tmp/hue$ git init +Initialized empty Git repository in ~/tmp/hue/.git/ +david@davids:~/tmp/hue$ git add -A +david@davids:~/tmp/hue$ git commit -m 'initial commit' +[master (root-commit) 67951dd] initial commit + 26 files changed, 887 insertions(+) + create mode 100644 .fixtures.yml + create mode 100644 .gitattributes + create mode 100644 .gitignore + create mode 100644 .gitlab-ci.yml + create mode 100644 .pdkignore + create mode 100644 .puppet-lint.rc + create mode 100644 .rspec + create mode 100644 .rubocop.yml + create mode 100644 .sync.yml + create mode 100644 .travis.yml + create mode 100644 .yardopts + create mode 100644 CHANGELOG.md + create mode 100644 Gemfile + create mode 100644 README.md + create mode 100644 Rakefile + create mode 100644 appveyor.yml + create mode 100644 data/common.yaml + create mode 100644 hiera.yaml + create mode 100644 lib/puppet/transport/hue.rb + create mode 100644 lib/puppet/transport/schema/hue.rb + create mode 100644 lib/puppet/util/network_device/hue/device.rb + create mode 100644 metadata.json + create mode 100644 spec/default_facts.yml + create mode 100644 spec/spec_helper.rb + create mode 100644 spec/unit/puppet/transport/hue_spec.rb + create mode 100644 spec/unit/puppet/transport/schema/hue_spec.rb +david@davids:~/tmp/hue$ +``` + +## Next up + +Once you have everything ready, head on to [implement the transport](./05-implementing-the-transport.md). diff --git a/docs/hands-on-lab/04-adding-a-new-transport/.sync.yml b/docs/hands-on-lab/04-adding-a-new-transport/.sync.yml new file mode 100644 index 00000000..2c6cc444 --- /dev/null +++ b/docs/hands-on-lab/04-adding-a-new-transport/.sync.yml @@ -0,0 +1,9 @@ +# use future defaults +--- +Gemfile: + optional: + ':development': + - gem: 'puppet-resource_api' +spec/spec_helper.rb: + mock_with: ':rspec' + diff --git a/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/hue.rb b/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/hue.rb new file mode 100644 index 00000000..c168c1f1 --- /dev/null +++ b/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/hue.rb @@ -0,0 +1,36 @@ +module Puppet::Transport + # The main connection class to a Hue endpoint + class Hue + # Initialise this transport with a set of credentials + def initialize(context, connection_info) + # because `password` is marked sensitive, we can log it here and it will be masked + context.debug("Connecting to #{connection_info[:user]}:#{connection_info[:password]}@#{connection_info[:host]}:#{connection_info[:port]}") + # store the credentials for later use + # alternatively, connect right here + @connection_info = connection_info + end + + # Verifies that the stored credentials are valid, and that we can talk to the target + def verify(context) + context.debug("Checking connection to #{@connection_info[:host]}:#{@connection_info[:port]}") + # in a real world implementation, the password would be checked by connecting + # to the target device or checking that an existing connection is still alive + raise 'authentication error' if @connection_info[:password].unwrap == 'invalid' + end + + # Retrieve facts from the target and return in a hash + def facts(context) + context.debug('Retrieving facts') + { + operatingsystem: 'example', + operatingsystemrelease: '1.2.3.4', + } + end + + # Close the connection and release all resources + def close(context) + context.debug('Closing connection') + @connection_info = nil + end + end +end diff --git a/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/schema/hue.rb b/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/schema/hue.rb new file mode 100644 index 00000000..56bf2388 --- /dev/null +++ b/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/schema/hue.rb @@ -0,0 +1,28 @@ +require 'puppet/resource_api' + +Puppet::ResourceApi.register_transport( + name: 'hue', + desc: <<-DESC, + This transport provides Puppet with the capability to connect to hue targets. + DESC + features: [], + connection_info: { + host: { + type: 'String', + desc: 'The hostname or IP address to connect to for this target.', + }, + port: { + type: 'Optional[Integer]', + desc: 'The port to connect to. Defaults to ...', + }, + user: { + type: 'String', + desc: 'The name of the user to authenticate as.', + }, + password: { + type: 'String', + desc: 'The password for the user.', + sensitive: true, + }, + }, +) diff --git a/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/util/network_device/hue/device.rb b/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/util/network_device/hue/device.rb new file mode 100644 index 00000000..b920c0cb --- /dev/null +++ b/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/util/network_device/hue/device.rb @@ -0,0 +1,15 @@ +require 'puppet/resource_api/transport/wrapper' + +# Initialize the NetworkDevice module if necessary +class Puppet::Util::NetworkDevice; end + +# The Hue module only contains the Device class to bridge from puppet's internals to the Transport. +# All the heavy lifting is done bye the Puppet::ResourceApi::Transport::Wrapper +module Puppet::Util::NetworkDevice::Hue + # Bridging from puppet to the hue transport + class Device < Puppet::ResourceApi::Transport::Wrapper + def initialize(url_or_config, _options = {}) + super('hue', url_or_config) + end + end +end diff --git a/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/hue_spec.rb b/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/hue_spec.rb new file mode 100644 index 00000000..ea567726 --- /dev/null +++ b/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/hue_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +require 'puppet/transport/hue' + +RSpec.describe Puppet::Transport::Hue do + subject(:transport) { described_class.new(context, connection_info) } + + let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } + let(:connection_info) do + { + host: 'api.example.com', + user: 'admin', + password: 'aih6cu6ohvohpahN', + } + end + + before(:each) do + allow(context).to receive(:debug) + end + + describe 'initialize(context, connection_info)' do + it { expect { transport }.not_to raise_error } + end + + describe 'verify(context)' do + context 'with valid credentials' do + it 'returns' do + expect { transport.verify(context) }.not_to raise_error + end + end + + context 'with invalid credentials' do + let(:connection_info) { super().merge(password: 'invalid') } + + it 'raises an error' do + expect { transport.verify(context) }.to raise_error RuntimeError, %r{authentication error} + end + end + end + + describe 'facts(context)' do + let(:facts) { transport.facts(context) } + + it 'returns basic facts' do + expect(facts).to include(:operatingsystem, :operatingsystemrelease) + end + end + + describe 'close(context)' do + it 'releases resources' do + transport.close(context) + + expect(transport.instance_variable_get(:@connection_info)).to be_nil + end + end +end diff --git a/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/schema/hue_spec.rb b/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/schema/hue_spec.rb new file mode 100644 index 00000000..39580318 --- /dev/null +++ b/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/schema/hue_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'puppet/transport/schema/hue' + +RSpec.describe 'the hue transport' do + it 'loads' do + expect(Puppet::ResourceApi::Transport.list['hue']).not_to be_nil + end +end diff --git a/docs/hands-on-lab/05-implementing-the-transport-hints.md b/docs/hands-on-lab/05-implementing-the-transport-hints.md new file mode 100644 index 00000000..0d5ba03a --- /dev/null +++ b/docs/hands-on-lab/05-implementing-the-transport-hints.md @@ -0,0 +1,19 @@ +## Implementing the transport - Exercise + +Implement a `request_debug` option that can be toggled on to create additional debug output on each request. You will not need concepts that you haven't yet seen in this tutorial, and basic programing skills. If you get stuck, have a look at some hints below, or [the finished file](TODO). + +## Hints + +* A toggle option can be created with the `Boolean` (`true` or `false`) data type. Add it to the `connection_info` in the transport schema. + +* Make it an `Optional[Boolean]` so that users who do not require request debugging do not have to specify the value. + +* To remember the value passed in from the user, store `connection_info[:request_debug]` in a `@request_debug` variable. + +* In the `hue_get` and `hue_put` methods add `context.debug(message)` calls showing the method's arguments. + +* Make the debugging optional based on the user's input by appending `if @request_debug` to each logging statement. + +# Next Up + +Now that the transport can talk to the remote target, it's time to [implement a provider](./06-implementing-the-provider.md). diff --git a/docs/hands-on-lab/05-implementing-the-transport.md b/docs/hands-on-lab/05-implementing-the-transport.md new file mode 100644 index 00000000..8f8f6f4a --- /dev/null +++ b/docs/hands-on-lab/05-implementing-the-transport.md @@ -0,0 +1,126 @@ +# Implementing the transport + +A transport consists of a *schema* describing the required data and credentials to connect to the HUE hub, and the *implementation* containing all the code to facilitate communication with the devices. + +## Schema + + The transport schema defines those attributes in a reusable manner, allowing users to understand the requirements of the transport. All schemas are located in `lib/puppet/transport/schema` in a ruby file named after the transport. In this case `hue.rb`. + +To connect to the HUE hub we need an IP address, a port, and an API key. + +Replace the `connection_info` in `lib/puppet/transport/schema/hue.rb` with the following snippet. + +```ruby + connection_info: { + host: { + type: 'String', + desc: 'The FQDN or IP address of the hue light system to connect to.', + }, + port: { + type: 'Optional[Integer]', + desc: 'The port to use when connecting, defaults to 80.', + }, + key: { + type: 'String', + desc: 'The access key that allows access to the hue light system.', + sensitive: true, + }, + }, +``` + +> Note: The Resource API transports use [Puppet Data Types](https://puppet.com/docs/puppet/5.3/lang_data_type.html#core-data-types) to define the allowable values for an attribute. Abstract types like `Optional[]` can be useful to make using your transport easier. Take special note of the `sensitive: true` annotation on the `key`; it instructs all services processing this attribute with special care, for example to avoid logging the key. + + +## Implementation + +The implementation of a transport provides connectivity and utility functions for both puppet and the providers managing the remote target. The HUE API is a simple REST interface, so we can store the credentials until we need make a connection. The default template at `lib/puppet/transport/hue.rb` already does this. Have a look at the `initialize` function to see how this is done. + +For the HUE's REST API, we want to create a `Faraday` object to capture the target host and key, so the transport can facilitate requests. Replace the `initialize` method in `lib/puppet/transport/hue.rb` with the following snippet: + + +``` + # @summary + # Initializes and returns a faraday connection to the given host + def initialize(_context, connection_info) + # provide a default port + port = connection_info[:port].nil? ? 80 : connection_info[:port] + Puppet.debug "Connecting to #{connection_info[:host]}:#{port} with dev key" + @connection = Faraday.new(url: "http://#{connection_info[:host]}:#{port}/api/#{connection_info[:key].unwrap}", ssl: { verify: false }) + end +``` + +> Note the `unwrap` call on building the URL, to access the sensitive value. + +### Facts + +The transport is also responsible for collecting any facts from the remote target, similar to how facter works for regular systems. For now we'll only return a hardcoded `operatingsystem` value to mark HUE Hubs: + +Replace the example `facts` method in `lib/puppet/transport/hue.rb` with this snippet: + +``` + # @summary + # Returns set facts regarding the HUE Hub + def facts(_context) + { 'operatingsystem' => 'philips_hue' } + end +``` + +### Connection Verification and Closing + +To enable better feedback when something goes wrong, a transport can implement a `verify` method to run extra checks on the credentials passed in. + +To save resources both on the target and the node running the transport, the `close` method will be called when the transport is not needed anymore. The transport can close connections and release memory and other resources at this point. + +For this tutorial, replace the example methods with this snippet: + +``` + # @summary + # Test that transport can talk to the remote target + def verify(_context) + end + + # @summary + # Close connection, free up resources + def close(_context) + @connection = nil + end +``` + +### Making requests + +Besides exposing some standardises functionality to puppet, the transport is also a good place to put utility functions that can be reused across your providers. While it may seem overkill for this small example, it is no extra effort, and will establish a healthy pattern. + +Insert the following snippet after the `close` method: + +``` + # @summary + # Make a get request to the HUE Hub API + def hue_get(context, url, args = nil) + url = URI.escape(url) if url + result = @connection.get(url, args) + JSON.parse(result.body) + rescue JSON::ParserError => e + raise Puppet::ResourceError, "Unable to parse JSON response from HUE API: #{e}" + end + + # @summary + # Sends an update command to the given url/connection + def hue_put(context, url, message) + message = message.to_json + @connection.put(url, message) + end +``` + +## Exercise + +Implement a `request_debug` option that can be toggled on to create additional debug output on each request. You will not need concepts that you haven't yet seen in this tutorial, and basic programing skills. If you get stuck, have a look at [some hints](./05-implementing-the-transport-hints.md), or [the finished file](TODO). + + +# Next Up + +Now that the transport can talk to the remote target, it's time to [implement a provider](./06-implementing-the-provider.md). diff --git a/docs/hands-on-lab/06-implementing-the-provider.md b/docs/hands-on-lab/06-implementing-the-provider.md new file mode 100644 index 00000000..21af475a --- /dev/null +++ b/docs/hands-on-lab/06-implementing-the-provider.md @@ -0,0 +1,201 @@ +# Implementing the Provider + +To expose resources from the HUE Hub to puppet, a type and provider define and implement the desired interactions. The *type*, like the transport schema, defines the shape of the data using Puppet Data Types. The implementation in the *provider* takes care of the communication and data transformation. + +For this hands on lab, we'll now go through implementing a simple `hue_light` type and provider to manage the state of the light bulbs connected to the HUE Hub. + +## Generating the Boilerplate + +In your module directory, run `pdk new provider hue_light`. This will create another set of files with a bare-bones type and provider, as well as unit tests. + +``` +david@davids:~/tmp/hue_workshop$ pdk new provider hue_light +pdk (INFO): Creating '/home/david/tmp/hue_workshop/lib/puppet/provider/hue_light/hue_light.rb' from template. +pdk (INFO): Creating '/home/david/tmp/hue_workshop/lib/puppet/type/hue_light.rb' from template. +pdk (INFO): Creating '/home/david/tmp/hue_workshop/spec/unit/puppet/provider/hue_light/hue_light_spec.rb' from template. +pdk (INFO): Creating '/home/david/tmp/hue_workshop/spec/unit/puppet/type/hue_light_spec.rb' from template. +david@davids:~/tmp/hue_workshop$ +``` + +## Defining the Type + +The type defines the attributes and allowed values, as well as a couple of other bits of information that concerns the processing of this provider. + +For remote resources like this, adding the `'remote_resource'` feature is necessary to alert puppet of its specific needs. Add the string to the existing `features` array. + +Browsing through the Hub API (TODO: insert link), we can identify a few basic properties we want to manage: + +* whether the lamp is on or off +* the colour of the light (hue and saturation) +* the brightness of the light + +To define the necessary attributes, insert the following snippet into the `attributes` hash, after the `name`: + +``` + on: { + type: 'Optional[Boolean]', + desc: 'Switches the light on or off', + }, + hue: { + type: 'Optional[Integer]', + desc: 'The hue the light color.', + }, + sat: { + type: 'Optional[Integer]', + desc: 'The saturation of the light colour', + }, + bri: { + type: 'Optional[Integer[1,254]]', + desc: < TODO: explain steps to gain access to API keys for real device + +``` +# hub1.conf +host: 192.168.43.195 +key: onmdTvd198bMrC6QYyVE9iasfYSeyAbAj3XyQzfL +``` + +``` +# device.conf +[hub1] +type hue +url file:///home/david/git/hue_workshop/spec/fixtures/hub1.conf + +[hub2] +type hue +url file:///home/david/git/hue_workshop/spec/fixtures/hub2.conf +``` + +``` +david@davids:~/tmp/hue_workshop$ pdk bundle install +pdk (INFO): Using Ruby 2.4.5 +pdk (INFO): Using Puppet 5.5.12 +[...] +Bundle complete! 10 Gemfile dependencies, 90 gems now installed. +Use `bundle info [gemname]` to see where a bundled gem is installed. + +david@davids:~/tmp/hue_workshop$ pdk bundle exec puppet device --libdir lib --deviceconfig device.conf --target hub1 --resource hue_light +pdk (INFO): Using Ruby 2.4.5 +pdk (INFO): Using Puppet 5.5.12 +hue_light { '1': + on => true, + bri => 37, + hue => 13393, + sat => 204, + effect => 'none', + alert => 'select', +} +hue_light { '2': + on => true, + bri => 37, + hue => 13401, + sat => 204, + effect => 'none', + alert => 'select', +} +hue_light { '3': + on => true, + bri => 254, + hue => 65136, + sat => 254, + effect => 'none', + alert => 'none', +} + +david@davids:~/tmp/hue_workshop$ +``` + +### Changing the state of the lights + +The final step here is to implement enforcing the desired state of the lights. The default template from the PDK offers `create`, `update`, and `delete` methods to implement the various operations. + +For the HUE Hub API, we can remove the `create` and `delete` method. Since the attribute names and data definitions line up with the HUE Hub API, the `update` method is very short. + +Replace the `create`, `update`, and `delete` methods with this snippet: + +``` + def update(context, name, should) + context.device.hue_put("lights/#{name}/state", should) + end +``` + +Now you can also change the state of the lights using a manifest: + +``` +# traffic_lights.pp +Hue_light { on => true, bri => 10, sat => 254 } +hue_light { + '1': + hue => 23536; + '2': + hue => 10000; + '3': + hue => 65136; +} +``` + +``` +david@davids:~/git/hue_workshop$ pdk bundle exec puppet device --libdir lib --deviceconfig device.conf --target hub1 --apply examples/traffic_lights.pp +pdk (INFO): Using Ruby 2.4.5 +pdk (INFO): Using Puppet 5.5.12 +Notice: Compiled catalog for hub1 in environment production in 0.06 seconds +Notice: /Stage[main]/Main/Hue_light[1]/hue: hue changed 13393 to 23536 (corrective) +Notice: /Stage[main]/Main/Hue_light[1]/bri: bri changed 70 to 10 (corrective) +Notice: /Stage[main]/Main/Hue_light[1]/sat: sat changed 204 to 255 (corrective) +Notice: /Stage[main]/Main/Hue_light[2]/hue: hue changed 13401 to 10000 (corrective) +Notice: /Stage[main]/Main/Hue_light[2]/bri: bri changed 70 to 10 (corrective) +Notice: /Stage[main]/Main/Hue_light[2]/sat: sat changed 204 to 255 (corrective) +Notice: /Stage[main]/Main/Hue_light[3]/bri: bri changed 254 to 10 (corrective) +Notice: /Stage[main]/Main/Hue_light[3]/sat: sat changed 254 to 255 (corrective) +Notice: Applied catalog in 0.18 seconds + +david@davids:~/git/hue_workshop$ +``` + +## Exercise + +To round out the API support, add a `effect` attribute that defaults to `none`, but can be set to `colorloop`, and an `alert` attribute that defaults to `none` and can be set to `select`. + +Note that this exercise will require exploring new data types and Resource API options. + +> TODO: add exercise hints + +# Next Up + +Now that we can manage state, it's time to [implement a task](./07-implementing-a-task.md) to do some fun transient things with the lights. diff --git a/docs/hands-on-lab/07-implementing-a-task.md b/docs/hands-on-lab/07-implementing-a-task.md new file mode 100644 index 00000000..bf691f32 --- /dev/null +++ b/docs/hands-on-lab/07-implementing-a-task.md @@ -0,0 +1,181 @@ +# Implementing a Task + +> TODO: this is NOT fine, yet + +* add bolt gem +``` +Gemfile: + optional: + ':development': + - gem: 'puppet-resource_api' + - gem: 'faraday' + # add this + - gem: 'bolt' +``` + + +``` +david@davids:~/tmp/hue_workshop$ pdk update --force +pdk (INFO): Updating david-hue_workshop using the default template, from 1.10.0 to 1.10.0 + +----------Files to be modified---------- +Gemfile + +---------------------------------------- + +You can find a report of differences in update_report.txt. + + +------------Update completed------------ + +1 files modified. + +david@davids:~/tmp/hue_workshop$ pdk bundle install +pdk (INFO): Using Ruby 2.5.3 +pdk (INFO): Using Puppet 6.4.2 +[...] +Bundle complete! 11 Gemfile dependencies, 122 gems now installed. +Use `bundle info [gemname]` to see where a bundled gem is installed. + +david@davids:~/tmp/hue_workshop$ +``` + +* add ruby_task_helper module + + +Using the development version: + +``` +fixtures: + # forge_modules: + # ruby_task_helper: "puppetlabs/ruby_task_helper" + repositories: + ruby_task_helper: + repo: "git://github.com/da-ar/puppetlabs-ruby_task_helper" + ref: "38745f8e7c2521c50bbf1b8e03318006cdac7a02" +``` + +``` +david@davids:~/tmp/hue_workshop$ pdk bundle exec rake spec_prep +pdk (INFO): Using Ruby 2.5.3 +pdk (INFO): Using Puppet 6.4.2 +HEAD is now at 38745f8 (FM-7955) Update to use Transport helper code +Cloning into 'spec/fixtures/modules/ruby_task_helper'... +I, [2019-06-04T13:43:58.577944 #9390] INFO -- : Creating symlink from spec/fixtures/modules/hue_workshop to /home/david/tmp/hue_workshop +david@davids:~/tmp/hue_workshop$ +``` + +* `pdk new task` based on https://github.com/puppetlabs/puppetlabs-panos/blob/master/tasks/apikey.rb + +``` +david@davids:~/tmp/hue_workshop$ pdk new task alarm +pdk (INFO): Creating '/home/david/tmp/hue_workshop/tasks/alarm.sh' from template. +pdk (INFO): Creating '/home/david/tmp/hue_workshop/tasks/alarm.json' from template. +david@davids:~/tmp/hue_workshop$ mv /home/david/tmp/hue_workshop/tasks/alarm.sh /home/david/tmp/hue_workshop/tasks/alarm.rb +david@davids:~/tmp/hue_workshop$ +``` + +* `tasks/alarm.json` +```json +{ + "puppet_task_version": 1, + "supports_noop": false, + "remote": true, + "description": "A short description of this task", + "parameters": { + "name": { + "type": "String", + "description": "The lamp to alarm" + } + }, + "files": [ + "ruby_task_helper/files/task_helper.rb", + "hue_workshop/lib/puppet/transport/hue.rb", + "hue_workshop/lib/puppet/transport/schema/hue.rb" + ] +} +``` + +* `tasks/alarm.rb` + +```ruby +#!/opt/puppetlabs/puppet/bin/ruby + +require 'puppet' +require_relative "../../ruby_task_helper/files/task_helper.rb" + +class AlarmTask < TaskHelper + def task(params = {}, remote = nil) + name = params[:name] + 5.times do |i| + remote.transport.hue_put("lights/#{name}/state", + name: name, + on: false, + ) + sleep 1.0 + remote.transport.hue_put("lights/#{name}/state", + name: name, + on: true, + hue: 10000*i, + sat: 255 + ) + sleep 1.0 + end + {} + end +end + +if __FILE__ == $0 + AlarmTask.run +end +``` + +* execute `pdk bundle exec bolt ...` + +```yaml +# inventory.yaml +--- +nodes: + - name: "192.168.43.195" + alias: hub1 + config: + transport: remote + remote: + remote-transport: hue + key: "onmdTvd198bMrC6QYyVE9iasfYSeyAbAj3XyQzfL" +``` + +``` +david@davids:~/tmp/hue_workshop$ pdk bundle exec bolt task run hue_workshop::alarm --modulepath spec/fixtures/modules/ --target hub1 --inventoryfile inventory.yaml +pdk (INFO): Using Ruby 2.5.3 +pdk (INFO): Using Puppet 6.4.2 +Started on 192.168.43.195... +Finished on 192.168.43.195: + { + } +Successful on 1 node: 192.168.43.195 +Ran on 1 node in 11.32 seconds + +david@davids:~/tmp/hue_workshop$ +``` + +* profit! From 57901eaee9a3082f94ed3fc805f37c1e4528072e Mon Sep 17 00:00:00 2001 From: clairecadman Date: Tue, 18 Jun 2019 12:07:35 +0100 Subject: [PATCH 2/4] (FM-8079) Docs edit --- docs/README.md | 6 ++-- docs/hands-on-lab/01-installing-prereqs.md | 12 ++++---- .../02-connecting-to-the-lightbulbs.md | 14 ++++----- docs/hands-on-lab/03-creating-a-new-module.md | 26 ++++++++-------- .../hands-on-lab/04-adding-a-new-transport.md | 10 +++---- .../05-implementing-the-transport-hints.md | 10 +++---- .../05-implementing-the-transport.md | 26 ++++++++-------- .../06-implementing-the-provider.md | 30 +++++++++---------- 8 files changed, 67 insertions(+), 67 deletions(-) diff --git a/docs/README.md b/docs/README.md index 1a30afb1..0c488871 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@ # Resource API hands-on lab -This lab will walk you through the basic steps of creating a native integration with puppet. After this you will have a fully functioning module to manage Philips HUE lights and have seen all the bits fall into place. +The Resource API hands-on lab walks you through creating a native integration with Puppet. After completely this lab, you will have a fully functioning module to manage Philips HUE lights. -These labs are intended for new and experienced developers alike. Please post feedback and suggestions for improvement in the [issues section](https://github.com/puppetlabs/puppet-resource_api/issues). +>Note: These labs are intended for both new and experienced developers. If you have any feedback or suggestions for improvement, post it in the [issues section](https://github.com/puppetlabs/puppet-resource_api/issues). -Start with [installing the Puppet Development Kit](./hands-on-lab/01-installing-prereqs.md) +To start with, we'll go through [installing Puppet Development Kit](./hands-on-lab/01-installing-prereqs.md)(PDK). diff --git a/docs/hands-on-lab/01-installing-prereqs.md b/docs/hands-on-lab/01-installing-prereqs.md index b4aae380..ef983b07 100644 --- a/docs/hands-on-lab/01-installing-prereqs.md +++ b/docs/hands-on-lab/01-installing-prereqs.md @@ -1,16 +1,16 @@ -# Install the Puppet Development Kit and other tools +# Install Puppet Development Kit (PDK) and other tools -To start out, install the Puppet Development Kit (PDK), which will provide all necessary tools and libraries to build and test modules. Additionally we recommend an emulator for the target device of this lab, a code editor with good ruby and puppet support, and GIT, a version control system to keep track of our progress. +To start, install Puppet Development Kit (PDK), which provides all the necessary tools and libraries to build and test modules. We also recommend an emulator for the target device, a code editor with good Ruby and Puppet support, and git — a version control system to keep track of your progress. -1. Choose your platform from https://puppet.com/download-puppet-development-kit; download and install the package +1. [Download PDK](https://puppet.com/download-puppet-development-kit) on your platform of choice. -2. If you do not have a Philips HUE hub available, you can download the [Hue-Emulator](https://github.com/SteveyO/Hue-Emulator/raw/master/HueEmulator-v0.8.jar). You will need to have Java installed to run this. +2. If you do not have a Philips HUE hub available, you can download the [Hue-Emulator](https://github.com/SteveyO/Hue-Emulator/raw/master/HueEmulator-v0.8.jar). You need to have Java installed to run this. 3. To edit code, we recommend the cross-platform editor [VSCode](https://code.visualstudio.com/download), with the [Ruby](https://marketplace.visualstudio.com/items?itemName=rebornix.Ruby) and [Puppet](https://marketplace.visualstudio.com/items?itemName=jpogran.puppet-vscode) extensions. There are lots of other extensions that can help you with your development workflow. -4. Git is a version control system that helps you keep track of changes and collaborate with others. In the course of the hands-on lab, we will show some integrations with cloud services that can help you. If you never used git before, skip this and all related steps for now. +4. Git is a version control system that helps you keep track of changes and collaborate with others. As we go through hands-on lab, we will show you some integrations with cloud services. If you have never used git before, ignore this and all related steps. ## Next up -Having installed all of this, let's [light up a few](./02-connecting-to-the-lightbulbs.md). +After installing the relevant tools, you'll [light up a few](./02-connecting-to-the-lightbulbs.md). diff --git a/docs/hands-on-lab/02-connecting-to-the-lightbulbs.md b/docs/hands-on-lab/02-connecting-to-the-lightbulbs.md index a7fbf125..f93be1cc 100644 --- a/docs/hands-on-lab/02-connecting-to-the-lightbulbs.md +++ b/docs/hands-on-lab/02-connecting-to-the-lightbulbs.md @@ -1,28 +1,28 @@ -# Connecting to the Lightbulbs +# Connecting to the light bulbs -While there are no technical restrictions on the kinds of remote devices or APIs you can connect to with transports, for the purpose of this workshop we are connecting to a Philips HUE hub and make some colourful wireless lightbulbs light up. If you (understandably) do not have physical devices available, you can get going with the Hue Emulator. +There are no technical restrictions on the kinds of remote devices or APIs you can connect to with transports. For this lab, we will connect to a Philips HUE hub and make some colourful wireless light bulbs light up. If you (understandably) do not have physical devices available, you can use the Hue Emulator. ## Hue Emulator Download the [HueEmulator-v0.8.jar](https://github.com/SteveyO/Hue-Emulator/blob/master/HueEmulator-v0.8.jar) from [SteveyO/Hue-Emulator](https://github.com/SteveyO/Hue-Emulator). -To run this emulator, you will need to have a Java Runtime installed. Once you have java installed, use `java -jar` with the emulator's filename to run it: +To run the Hue Emulator, you need to have a Java Runtime installed. Use `java -jar` with the emulator's filename to run it, for example: ``` david@davids:~$ java -jar ~/Downloads/HueEmulator-v0.8.jar ``` -It will not produce any output on the command line, but it will pop up a window with a hub and a few predefined lights: +It does not produce any output on the command line, but a window pops up with a hub and a few predefined lights: ![](./02-connecting-to-the-lightbulbs-emulator.png) -All you need now is to input a port (the default 8000 is usually fine) and press "Start" to activate the built-in server. +All you need now is to input a port (the default 8000 is usually fine) and click "Start" to activate the built-in server. ## Connecting to your hub -To connect to an actual hub, you will need be able to access the Hub on your network and get an API key. Follow the [Philips Developer docs](http://www.developers.meethue.com/documentation/getting-started) (registration required) for that. +To connect to an actual hub, you need be able to access the bub on your network and get an API key. See the [Philips Developer docs](http://www.developers.meethue.com/documentation/getting-started) (registration required). ## Next up -Once you have some lights up, head on to [create a new module](./03-creating-a-new-module.md). +Now that you have some lights up, you'll [create a module](./03-creating-a-new-module.md). diff --git a/docs/hands-on-lab/03-creating-a-new-module.md b/docs/hands-on-lab/03-creating-a-new-module.md index 3bb34dbb..df758488 100644 --- a/docs/hands-on-lab/03-creating-a-new-module.md +++ b/docs/hands-on-lab/03-creating-a-new-module.md @@ -1,26 +1,26 @@ -# Create a new module +# Create a module -Depending on your preferences, you can use the VSCode/PDK integration or run the PDK from the command line in a terminal of your choice. +Depending on your preferences, you can use the VSCode/PDK integration or run PDK from the command line in a terminal of your choice. -## VSCode +## Create a module with VSCode -Hit up the Command Palette (⇧⌘P on the Mac or Ctrl+Shift+P on Windows and Linux) and search for the `Puppet: PDK New Module` task: +Spin up the Command Palette (⇧⌘P on the Mac or Ctrl+Shift+P on Windows and Linux) and search for the `Puppet: PDK New Module` task: ![](./03-creating-a-new-module_vscode.png) -Press Enter (↩) to execute this and follow the on-screen prompts. +Click Enter (↩) to execute this and follow the on-screen prompts. The module will open in a new VSCode window. -## Command Line +## Create a module from the command line -In your regular workspace (for example your home directory) run +In your regular workspace (for example your home directory), run the following: ``` pdk new module hue_workshop --skip-interview ``` -This will create a new module `hue_workshop` in a directory of the same name, using all defaults. Output should look like the following: +This command creates a new module `hue_workshop` in your directory of the same name, using all defaults. The output will look like: ``` david@davids:~/tmp$ pdk new module hue_workshop --skip-interview @@ -33,15 +33,15 @@ CHANGELOG.md examples Gemfile hiera.yaml metadata.json README.md tasks david@davids:~/tmp$ ``` -To read more about the different options when creating new modules see [the PDK docs](https://puppet.com/docs/pdk/1.x/pdk_creating_modules.html). +To read more about the different options when creating new modules, see [PDK docs](https://puppet.com/docs/pdk/1.x/pdk_creating_modules.html). -Open the new directory in VSCode, or your coding editor of choice: +Open the new directory in your code editor: ``` code -a hue_workshop ``` -For this workshop, we'll active a few future defaults to make our lives easier down the line. Directly in the `hue_workshop` directory, create a file called `.sync.yml` and paste the following snippet: +Next, we'll active a few future defaults. In the `hue_workshop` directory, create a file called `.sync.yml` and paste the following: ``` # .sync.yml @@ -56,7 +56,7 @@ spec/spec_helper.rb: mock_with: ':rspec' ``` -Then run `pdk update` in the module's directory to deploy the changes into the module: +Run `pdk update` in the module's directory to deploy the changes in the module: ``` david@davids:~/tmp/hue_workshop$ pdk update @@ -82,4 +82,4 @@ david@davids:~/tmp/hue_workshop$ ## Next up -Once you have the module ready, head on to [add a transport](./04-adding-a-new-transport.md). +Now that you have created a module, you'll [add a transport](./04-adding-a-new-transport.md). diff --git a/docs/hands-on-lab/04-adding-a-new-transport.md b/docs/hands-on-lab/04-adding-a-new-transport.md index 36560b42..3569d9c1 100644 --- a/docs/hands-on-lab/04-adding-a-new-transport.md +++ b/docs/hands-on-lab/04-adding-a-new-transport.md @@ -1,6 +1,6 @@ # Add a new transport -[Eventually](https://github.com/puppetlabs/pdk/pull/666) there will be a `pdk new transport`, for now you'll need to copy in a few files for this workshop. +[Eventually](https://github.com/puppetlabs/pdk/pull/666) there will be a `pdk new transport` command. For now, you'll need to copy the files below. Copy the files from [this directory](./04-adding-a-new-transport/) into your new module: @@ -11,7 +11,7 @@ Copy the files from [this directory](./04-adding-a-new-transport/) into your new * spec/unit/puppet/transport/hue_spec.rb * spec/unit/puppet/transport/schema/hue_spec.rb -Afterwards run `pdk update --force` to enable a few future defaults that are required for these templates: +Run `pdk update --force` to enable a few future defaults that are required for these templates: ``` david@davids:~/tmp/hue$ pdk update --force @@ -36,7 +36,7 @@ david@davids:~/tmp/hue$ ## Checkpoint -To make sure that everything went well with creating the module and copying in the files you can run `pdk validate --parallel` and `pdk test unit`: +To validate your new module and transport, run `pdk validate --parallel` and `pdk test unit`: ``` david@davids:~/tmp/hue$ pdk validate --parallel @@ -62,7 +62,7 @@ Run options: exclude {:bolt=>true} david@davids:~/tmp/hue$ ``` -If you're working with a version control system, this would also be a good point to make the first commit to store away all the boilerplate code and later revisit the changes you made. Here I'm showing how to initialise a local git repository and storing all files in an initial commit: +If you're working with a version control system, now would be a good time to make your first commit and store the boilerplate code, and then you can revisit the changes you made later. For example: ``` david@davids:~/tmp/hue$ git init @@ -102,4 +102,4 @@ david@davids:~/tmp/hue$ ## Next up -Once you have everything ready, head on to [implement the transport](./05-implementing-the-transport.md). +Now that you have everything ready, you'll [implement the transport](./05-implementing-the-transport.md). diff --git a/docs/hands-on-lab/05-implementing-the-transport-hints.md b/docs/hands-on-lab/05-implementing-the-transport-hints.md index 0d5ba03a..f73ebc22 100644 --- a/docs/hands-on-lab/05-implementing-the-transport-hints.md +++ b/docs/hands-on-lab/05-implementing-the-transport-hints.md @@ -1,18 +1,18 @@ ## Implementing the transport - Exercise -Implement a `request_debug` option that can be toggled on to create additional debug output on each request. You will not need concepts that you haven't yet seen in this tutorial, and basic programing skills. If you get stuck, have a look at some hints below, or [the finished file](TODO). +Implement the `request_debug` option that you can toggle on to create additional debug output on each request. If you get stuck, review the hints below, or [the finished file](TODO). ## Hints -* A toggle option can be created with the `Boolean` (`true` or `false`) data type. Add it to the `connection_info` in the transport schema. +* You can create a toggle option with the `Boolean` (`true` or `false`) data type. Add it to the `connection_info` in the transport schema. * Make it an `Optional[Boolean]` so that users who do not require request debugging do not have to specify the value. -* To remember the value passed in from the user, store `connection_info[:request_debug]` in a `@request_debug` variable. +* To remember the value you passed, store `connection_info[:request_debug]` in a `@request_debug` variable. -* In the `hue_get` and `hue_put` methods add `context.debug(message)` calls showing the method's arguments. +* In the `hue_get` and `hue_put` methods, add `context.debug(message)` calls showing the method's arguments. -* Make the debugging optional based on the user's input by appending `if @request_debug` to each logging statement. +* Make the debugging optional based on your input by appending `if @request_debug` to each logging statement. # Next Up diff --git a/docs/hands-on-lab/05-implementing-the-transport.md b/docs/hands-on-lab/05-implementing-the-transport.md index 8f8f6f4a..bb2ac49b 100644 --- a/docs/hands-on-lab/05-implementing-the-transport.md +++ b/docs/hands-on-lab/05-implementing-the-transport.md @@ -4,11 +4,11 @@ A transport consists of a *schema* describing the required data and credentials ## Schema - The transport schema defines those attributes in a reusable manner, allowing users to understand the requirements of the transport. All schemas are located in `lib/puppet/transport/schema` in a ruby file named after the transport. In this case `hue.rb`. +The transport schema defines attributes in a reusable way, allowing you to understand the requirements of the transport. All schemas are located in `lib/puppet/transport/schema` in a Ruby file named after the transport. In this case `hue.rb`. -To connect to the HUE hub we need an IP address, a port, and an API key. +To connect to the HUE hub you need an IP address, a port, and an API key. -Replace the `connection_info` in `lib/puppet/transport/schema/hue.rb` with the following snippet. +Replace the `connection_info` in `lib/puppet/transport/schema/hue.rb` with the following code: ```ruby connection_info: { @@ -28,21 +28,21 @@ Replace the `connection_info` in `lib/puppet/transport/schema/hue.rb` with the f }, ``` -> Note: The Resource API transports use [Puppet Data Types](https://puppet.com/docs/puppet/5.3/lang_data_type.html#core-data-types) to define the allowable values for an attribute. Abstract types like `Optional[]` can be useful to make using your transport easier. Take special note of the `sensitive: true` annotation on the `key`; it instructs all services processing this attribute with special care, for example to avoid logging the key. +> Note: The Resource API transports use [Puppet Data Types](https://puppet.com/docs/puppet/5.3/lang_data_type.html#core-data-types) to define the allowable values for an attribute. Abstract types like `Optional[]` can be useful to make using your transport easier. Take note of the `sensitive: true` annotation on the `key`; it instructs all services processing this attribute with special care, for example to avoid logging the key. ## Implementation -The implementation of a transport provides connectivity and utility functions for both puppet and the providers managing the remote target. The HUE API is a simple REST interface, so we can store the credentials until we need make a connection. The default template at `lib/puppet/transport/hue.rb` already does this. Have a look at the `initialize` function to see how this is done. +The implementation of a transport provides connectivity and utility functions for both Puppet and the providers managing the remote target. The HUE API is a simple REST interface, so you can store the credentials until you need make a connection. The default template at `lib/puppet/transport/hue.rb` already does this. Have a look at the `initialize` function to see how this is done. -For the HUE's REST API, we want to create a `Faraday` object to capture the target host and key, so the transport can facilitate requests. Replace the `initialize` method in `lib/puppet/transport/hue.rb` with the following snippet: +For the HUE's REST API, we want to create a `Faraday` object to capture the target host and key so that the transport can facilitate requests. Replace the `initialize` method in `lib/puppet/transport/hue.rb` with the following code: +```--> ``` # @summary # Initializes and returns a faraday connection to the given host @@ -60,7 +60,7 @@ For the HUE's REST API, we want to create a `Faraday` object to capture the targ The transport is also responsible for collecting any facts from the remote target, similar to how facter works for regular systems. For now we'll only return a hardcoded `operatingsystem` value to mark HUE Hubs: -Replace the example `facts` method in `lib/puppet/transport/hue.rb` with this snippet: +Replace the example `facts` method in `lib/puppet/transport/hue.rb` with the following code: ``` # @summary @@ -70,13 +70,13 @@ Replace the example `facts` method in `lib/puppet/transport/hue.rb` with this sn end ``` -### Connection Verification and Closing +### Connection verification and closing To enable better feedback when something goes wrong, a transport can implement a `verify` method to run extra checks on the credentials passed in. To save resources both on the target and the node running the transport, the `close` method will be called when the transport is not needed anymore. The transport can close connections and release memory and other resources at this point. -For this tutorial, replace the example methods with this snippet: +For this tutorial, replace the example methods with the following code: ``` # @summary @@ -93,9 +93,9 @@ For this tutorial, replace the example methods with this snippet: ### Making requests -Besides exposing some standardises functionality to puppet, the transport is also a good place to put utility functions that can be reused across your providers. While it may seem overkill for this small example, it is no extra effort, and will establish a healthy pattern. +Besides exposing some standardises functionality to Puppet, the transport is also a good place to put utility functions that can be reused across your providers. While it may seem overkill for this small example, it is no extra effort, and will establish a healthy pattern. -Insert the following snippet after the `close` method: +Insert the following code after the `close` method: ``` # @summary @@ -118,7 +118,7 @@ Insert the following snippet after the `close` method: ## Exercise -Implement a `request_debug` option that can be toggled on to create additional debug output on each request. You will not need concepts that you haven't yet seen in this tutorial, and basic programing skills. If you get stuck, have a look at [some hints](./05-implementing-the-transport-hints.md), or [the finished file](TODO). +Implement a `request_debug` option that you can toggle to create additional debug output on each request. If you get stuck, have a look at [some hints](./05-implementing-the-transport-hints.md), or [the finished file](TODO). # Next Up diff --git a/docs/hands-on-lab/06-implementing-the-provider.md b/docs/hands-on-lab/06-implementing-the-provider.md index 21af475a..d177ba75 100644 --- a/docs/hands-on-lab/06-implementing-the-provider.md +++ b/docs/hands-on-lab/06-implementing-the-provider.md @@ -1,12 +1,12 @@ -# Implementing the Provider +# Implementing the provider -To expose resources from the HUE Hub to puppet, a type and provider define and implement the desired interactions. The *type*, like the transport schema, defines the shape of the data using Puppet Data Types. The implementation in the *provider* takes care of the communication and data transformation. +To expose resources from the HUE Hub to Puppet, a type and provider define and implement the desired interactions. The *type*, like the transport schema, defines the shape of the data using Puppet data types. The implementation in the *provider* takes care of the communication and data transformation. For this hands on lab, we'll now go through implementing a simple `hue_light` type and provider to manage the state of the light bulbs connected to the HUE Hub. ## Generating the Boilerplate -In your module directory, run `pdk new provider hue_light`. This will create another set of files with a bare-bones type and provider, as well as unit tests. +In your module directory, run `pdk new provider hue_light`. This creates another set of files with a bare-bones type and provider, as well as unit tests. ``` david@davids:~/tmp/hue_workshop$ pdk new provider hue_light @@ -17,17 +17,17 @@ pdk (INFO): Creating '/home/david/tmp/hue_workshop/spec/unit/puppet/type/hue_lig david@davids:~/tmp/hue_workshop$ ``` -## Defining the Type +## Defining the type The type defines the attributes and allowed values, as well as a couple of other bits of information that concerns the processing of this provider. -For remote resources like this, adding the `'remote_resource'` feature is necessary to alert puppet of its specific needs. Add the string to the existing `features` array. +For remote resources like this, adding the `'remote_resource'` feature is necessary to alert Puppet of its specific needs. Add the string to the existing `features` array. -Browsing through the Hub API (TODO: insert link), we can identify a few basic properties we want to manage: +Browsing through the Hub API (TODO: insert link), we can identify a few basic properties we want to manage, for example: -* whether the lamp is on or off -* the colour of the light (hue and saturation) -* the brightness of the light +* Whether the lamp is on or off +* The colour of the light (hue and saturation) +* The brightness of the light To define the necessary attributes, insert the following snippet into the `attributes` hash, after the `name`: @@ -54,11 +54,11 @@ DESC ## Implementing the Provider -Every provider needs a `get` method, that returns a list of currently existing resources and their attributes from the remote target. For the HUE Hub, this is requires a call to the `lights` endpoint and some data transformation to the format puppet expects. +Every provider needs a `get` method, that returns a list of currently existing resources and their attributes from the remote target. For the HUE Hub, this is requires a call to the `lights` endpoint and some data transformation to the format Puppet expects. ### Reading the state of the lights -Replace the example `get` function in `lib/puppet/provider/hue_light/hue_light.rb` with the following snippet: +Replace the example `get` function in `lib/puppet/provider/hue_light/hue_light.rb` with the following code: ``` # @summary @@ -81,7 +81,7 @@ Replace the example `get` function in `lib/puppet/provider/hue_light/hue_light.r end ``` -This method will return all connected lights from the HUE Hub and allow puppet to process those. To try this out, you need to setup a test configuration and then `puppet device` can be used to drive your testing. +This method returns all connected lights from the HUE Hub and allows Puppet to process them. To try this out, you need to setup a test configuration and use `puppet device` to drive your testing. > TODO: explain steps to gain access to API keys for real device @@ -147,7 +147,7 @@ The final step here is to implement enforcing the desired state of the lights. T For the HUE Hub API, we can remove the `create` and `delete` method. Since the attribute names and data definitions line up with the HUE Hub API, the `update` method is very short. -Replace the `create`, `update`, and `delete` methods with this snippet: +Replace the `create`, `update`, and `delete` methods with the following code: ``` def update(context, name, should) @@ -190,9 +190,9 @@ david@davids:~/git/hue_workshop$ ## Exercise -To round out the API support, add a `effect` attribute that defaults to `none`, but can be set to `colorloop`, and an `alert` attribute that defaults to `none` and can be set to `select`. +To round out the API support, add an `effect` attribute that defaults to `none`, but can be set to `colorloop`, and an `alert` attribute that defaults to `none` and can be set to `select`. -Note that this exercise will require exploring new data types and Resource API options. +Note that this exercise requires exploring new data types and Resource API options. > TODO: add exercise hints From 71a8e0b2bd8310dc1f9cc2a45ac156be6d45a8ac Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 24 Sep 2019 14:57:58 +0100 Subject: [PATCH 3/4] (FM-8079) Final touches * clarify that, yes, bulbs are needed, too * remove duplicate java and emulator install instructions * update "Add a new transport" section to use released pdk feature --- docs/hands-on-lab/01-installing-prereqs.md | 2 +- .../02-connecting-to-the-lightbulbs.md | 4 +- .../hands-on-lab/04-adding-a-new-transport.md | 28 ++++++---- .../lib/puppet/transport/hue.rb | 36 ------------ .../lib/puppet/transport/schema/hue.rb | 28 ---------- .../puppet/util/network_device/hue/device.rb | 15 ----- .../spec/unit/puppet/transport/hue_spec.rb | 56 ------------------- .../unit/puppet/transport/schema/hue_spec.rb | 8 --- 8 files changed, 18 insertions(+), 159 deletions(-) delete mode 100644 docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/hue.rb delete mode 100644 docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/schema/hue.rb delete mode 100644 docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/util/network_device/hue/device.rb delete mode 100644 docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/hue_spec.rb delete mode 100644 docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/schema/hue_spec.rb diff --git a/docs/hands-on-lab/01-installing-prereqs.md b/docs/hands-on-lab/01-installing-prereqs.md index ef983b07..0eccb071 100644 --- a/docs/hands-on-lab/01-installing-prereqs.md +++ b/docs/hands-on-lab/01-installing-prereqs.md @@ -4,7 +4,7 @@ To start, install Puppet Development Kit (PDK), which provides all the necessary 1. [Download PDK](https://puppet.com/download-puppet-development-kit) on your platform of choice. -2. If you do not have a Philips HUE hub available, you can download the [Hue-Emulator](https://github.com/SteveyO/Hue-Emulator/raw/master/HueEmulator-v0.8.jar). You need to have Java installed to run this. +2. If you do not have a Philips HUE hub and bulbs available, you can download the [Hue-Emulator](https://github.com/SteveyO/Hue-Emulator/raw/master/HueEmulator-v0.8.jar). You need to have Java installed to run this. 3. To edit code, we recommend the cross-platform editor [VSCode](https://code.visualstudio.com/download), with the [Ruby](https://marketplace.visualstudio.com/items?itemName=rebornix.Ruby) and [Puppet](https://marketplace.visualstudio.com/items?itemName=jpogran.puppet-vscode) extensions. There are lots of other extensions that can help you with your development workflow. diff --git a/docs/hands-on-lab/02-connecting-to-the-lightbulbs.md b/docs/hands-on-lab/02-connecting-to-the-lightbulbs.md index f93be1cc..8b943ce4 100644 --- a/docs/hands-on-lab/02-connecting-to-the-lightbulbs.md +++ b/docs/hands-on-lab/02-connecting-to-the-lightbulbs.md @@ -4,9 +4,7 @@ There are no technical restrictions on the kinds of remote devices or APIs you c ## Hue Emulator -Download the [HueEmulator-v0.8.jar](https://github.com/SteveyO/Hue-Emulator/blob/master/HueEmulator-v0.8.jar) from [SteveyO/Hue-Emulator](https://github.com/SteveyO/Hue-Emulator). - -To run the Hue Emulator, you need to have a Java Runtime installed. Use `java -jar` with the emulator's filename to run it, for example: +Use `java -jar` with the emulator's filename to run it, for example: ``` david@davids:~$ java -jar ~/Downloads/HueEmulator-v0.8.jar diff --git a/docs/hands-on-lab/04-adding-a-new-transport.md b/docs/hands-on-lab/04-adding-a-new-transport.md index 3569d9c1..738f50bf 100644 --- a/docs/hands-on-lab/04-adding-a-new-transport.md +++ b/docs/hands-on-lab/04-adding-a-new-transport.md @@ -1,17 +1,9 @@ # Add a new transport -[Eventually](https://github.com/puppetlabs/pdk/pull/666) there will be a `pdk new transport` command. For now, you'll need to copy the files below. +Starting with PDK 1.12.0 there is the `pdk new transport` command, that you can use to create the base files for your new transport: -Copy the files from [this directory](./04-adding-a-new-transport/) into your new module: - -* .sync.yml -* lib/puppet/transport/hue.rb -* lib/puppet/transport/schema/hue.rb -* lib/puppet/util/network_device/hue/device.rb -* spec/unit/puppet/transport/hue_spec.rb -* spec/unit/puppet/transport/schema/hue_spec.rb - -Run `pdk update --force` to enable a few future defaults that are required for these templates: +Copy the [.sync.yml](./04-adding-a-new-transport/.sync.yml) into your new module, and run `pdk update --force` to enable a few future +defaults that are required for this command: ``` david@davids:~/tmp/hue$ pdk update --force @@ -34,6 +26,18 @@ You can find a report of differences in update_report.txt. david@davids:~/tmp/hue$ ``` +Then, create the actual transport: + +``` +david@davids:~/tmp/hue$ pdk new transport hue +pdk (INFO): Creating '/home/david/tmp/hue/lib/puppet/transport/hue.rb' from template. +pdk (INFO): Creating '/home/david/tmp/hue/lib/puppet/transport/schema/hue.rb' from template. +pdk (INFO): Creating '/home/david/tmp/hue/lib/puppet/util/network_device/hue/device.rb' from template. +pdk (INFO): Creating '/home/david/tmp/hue/spec/unit/puppet/transport/hue_spec.rb' from template. +pdk (INFO): Creating '/home/david/tmp/hue/spec/unit/puppet/transport/schema/hue_spec.rb' from template. +david@davids:~/tmp/hue$ +``` + ## Checkpoint To validate your new module and transport, run `pdk validate --parallel` and `pdk test unit`: @@ -62,7 +66,7 @@ Run options: exclude {:bolt=>true} david@davids:~/tmp/hue$ ``` -If you're working with a version control system, now would be a good time to make your first commit and store the boilerplate code, and then you can revisit the changes you made later. For example: +If you're working with a version control system, now would be a good time to make your first commit and store the boilerplate code, and then you can revisit the changes you made later. For example: ``` david@davids:~/tmp/hue$ git init diff --git a/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/hue.rb b/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/hue.rb deleted file mode 100644 index c168c1f1..00000000 --- a/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/hue.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Puppet::Transport - # The main connection class to a Hue endpoint - class Hue - # Initialise this transport with a set of credentials - def initialize(context, connection_info) - # because `password` is marked sensitive, we can log it here and it will be masked - context.debug("Connecting to #{connection_info[:user]}:#{connection_info[:password]}@#{connection_info[:host]}:#{connection_info[:port]}") - # store the credentials for later use - # alternatively, connect right here - @connection_info = connection_info - end - - # Verifies that the stored credentials are valid, and that we can talk to the target - def verify(context) - context.debug("Checking connection to #{@connection_info[:host]}:#{@connection_info[:port]}") - # in a real world implementation, the password would be checked by connecting - # to the target device or checking that an existing connection is still alive - raise 'authentication error' if @connection_info[:password].unwrap == 'invalid' - end - - # Retrieve facts from the target and return in a hash - def facts(context) - context.debug('Retrieving facts') - { - operatingsystem: 'example', - operatingsystemrelease: '1.2.3.4', - } - end - - # Close the connection and release all resources - def close(context) - context.debug('Closing connection') - @connection_info = nil - end - end -end diff --git a/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/schema/hue.rb b/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/schema/hue.rb deleted file mode 100644 index 56bf2388..00000000 --- a/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/transport/schema/hue.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'puppet/resource_api' - -Puppet::ResourceApi.register_transport( - name: 'hue', - desc: <<-DESC, - This transport provides Puppet with the capability to connect to hue targets. - DESC - features: [], - connection_info: { - host: { - type: 'String', - desc: 'The hostname or IP address to connect to for this target.', - }, - port: { - type: 'Optional[Integer]', - desc: 'The port to connect to. Defaults to ...', - }, - user: { - type: 'String', - desc: 'The name of the user to authenticate as.', - }, - password: { - type: 'String', - desc: 'The password for the user.', - sensitive: true, - }, - }, -) diff --git a/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/util/network_device/hue/device.rb b/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/util/network_device/hue/device.rb deleted file mode 100644 index b920c0cb..00000000 --- a/docs/hands-on-lab/04-adding-a-new-transport/lib/puppet/util/network_device/hue/device.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'puppet/resource_api/transport/wrapper' - -# Initialize the NetworkDevice module if necessary -class Puppet::Util::NetworkDevice; end - -# The Hue module only contains the Device class to bridge from puppet's internals to the Transport. -# All the heavy lifting is done bye the Puppet::ResourceApi::Transport::Wrapper -module Puppet::Util::NetworkDevice::Hue - # Bridging from puppet to the hue transport - class Device < Puppet::ResourceApi::Transport::Wrapper - def initialize(url_or_config, _options = {}) - super('hue', url_or_config) - end - end -end diff --git a/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/hue_spec.rb b/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/hue_spec.rb deleted file mode 100644 index ea567726..00000000 --- a/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/hue_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -require 'puppet/transport/hue' - -RSpec.describe Puppet::Transport::Hue do - subject(:transport) { described_class.new(context, connection_info) } - - let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } - let(:connection_info) do - { - host: 'api.example.com', - user: 'admin', - password: 'aih6cu6ohvohpahN', - } - end - - before(:each) do - allow(context).to receive(:debug) - end - - describe 'initialize(context, connection_info)' do - it { expect { transport }.not_to raise_error } - end - - describe 'verify(context)' do - context 'with valid credentials' do - it 'returns' do - expect { transport.verify(context) }.not_to raise_error - end - end - - context 'with invalid credentials' do - let(:connection_info) { super().merge(password: 'invalid') } - - it 'raises an error' do - expect { transport.verify(context) }.to raise_error RuntimeError, %r{authentication error} - end - end - end - - describe 'facts(context)' do - let(:facts) { transport.facts(context) } - - it 'returns basic facts' do - expect(facts).to include(:operatingsystem, :operatingsystemrelease) - end - end - - describe 'close(context)' do - it 'releases resources' do - transport.close(context) - - expect(transport.instance_variable_get(:@connection_info)).to be_nil - end - end -end diff --git a/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/schema/hue_spec.rb b/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/schema/hue_spec.rb deleted file mode 100644 index 39580318..00000000 --- a/docs/hands-on-lab/04-adding-a-new-transport/spec/unit/puppet/transport/schema/hue_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -require 'spec_helper' -require 'puppet/transport/schema/hue' - -RSpec.describe 'the hue transport' do - it 'loads' do - expect(Puppet::ResourceApi::Transport.list['hue']).not_to be_nil - end -end From c7642a550c340527bcab073566d439d9ac4d59d3 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 30 Sep 2019 11:02:45 +0100 Subject: [PATCH 4/4] (FM-8079) Remove duplicate .sync.yml deployment --- docs/hands-on-lab/03-creating-a-new-module.md | 38 ------------------- .../hands-on-lab/04-adding-a-new-transport.md | 26 ++++++++++--- .../04-adding-a-new-transport/.sync.yml | 9 ----- 3 files changed, 20 insertions(+), 53 deletions(-) delete mode 100644 docs/hands-on-lab/04-adding-a-new-transport/.sync.yml diff --git a/docs/hands-on-lab/03-creating-a-new-module.md b/docs/hands-on-lab/03-creating-a-new-module.md index df758488..b8a726e5 100644 --- a/docs/hands-on-lab/03-creating-a-new-module.md +++ b/docs/hands-on-lab/03-creating-a-new-module.md @@ -41,44 +41,6 @@ Open the new directory in your code editor: code -a hue_workshop ``` -Next, we'll active a few future defaults. In the `hue_workshop` directory, create a file called `.sync.yml` and paste the following: - -``` -# .sync.yml ---- -Gemfile: - optional: - ':development': - - gem: 'puppet-resource_api' - - gem: 'faraday' - - gem: 'rspec-json_expectations' -spec/spec_helper.rb: - mock_with: ':rspec' -``` - -Run `pdk update` in the module's directory to deploy the changes in the module: - -``` -david@davids:~/tmp/hue_workshop$ pdk update -pdk (INFO): Updating david-hue_workshop using the default template, from 1.10.0 to 1.10.0 - -----------Files to be modified---------- -Gemfile -spec/spec_helper.rb - ----------------------------------------- - -You can find a report of differences in update_report.txt. - -Do you want to continue and make these changes to your module? Yes - -------------Update completed------------ - -2 files modified. - -david@davids:~/tmp/hue_workshop$ -``` - ## Next up diff --git a/docs/hands-on-lab/04-adding-a-new-transport.md b/docs/hands-on-lab/04-adding-a-new-transport.md index 738f50bf..759d41be 100644 --- a/docs/hands-on-lab/04-adding-a-new-transport.md +++ b/docs/hands-on-lab/04-adding-a-new-transport.md @@ -2,12 +2,26 @@ Starting with PDK 1.12.0 there is the `pdk new transport` command, that you can use to create the base files for your new transport: -Copy the [.sync.yml](./04-adding-a-new-transport/.sync.yml) into your new module, and run `pdk update --force` to enable a few future -defaults that are required for this command: +Next, we'll active a few future defaults. In the `hue_workshop` directory, create a file called `.sync.yml` and paste the following: ``` -david@davids:~/tmp/hue$ pdk update --force -pdk (INFO): Updating david-hue using the default template, from master@c43fc26 to master@c43fc26 +# .sync.yml +--- +Gemfile: + optional: + ':development': + - gem: 'puppet-resource_api' + - gem: 'faraday' + - gem: 'rspec-json_expectations' +spec/spec_helper.rb: + mock_with: ':rspec' +``` + +Run `pdk update` in the module's directory to deploy the changes in the module: + +``` +david@davids:~/tmp/hue_workshop$ pdk update --force +pdk (INFO): Updating david-hue_workshop using the default template, from 1.10.0 to 1.10.0 ----------Files to be modified---------- Gemfile @@ -17,13 +31,13 @@ spec/spec_helper.rb You can find a report of differences in update_report.txt. -[✔] Resolving default Gemfile dependencies. +Do you want to continue and make these changes to your module? Yes ------------Update completed------------ 2 files modified. -david@davids:~/tmp/hue$ +david@davids:~/tmp/hue_workshop$ ``` Then, create the actual transport: diff --git a/docs/hands-on-lab/04-adding-a-new-transport/.sync.yml b/docs/hands-on-lab/04-adding-a-new-transport/.sync.yml deleted file mode 100644 index 2c6cc444..00000000 --- a/docs/hands-on-lab/04-adding-a-new-transport/.sync.yml +++ /dev/null @@ -1,9 +0,0 @@ -# use future defaults ---- -Gemfile: - optional: - ':development': - - gem: 'puppet-resource_api' -spec/spec_helper.rb: - mock_with: ':rspec' -