Skip to content

Commit 7fd8724

Browse files
authored
Merge pull request #181 from DavidS/hands-on-lab
(FM-8079) Resource API and Transports Hands-on-Lab
2 parents 19eaa06 + c7642a5 commit 7fd8724

11 files changed

+746
-0
lines changed

docs/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Resource API hands-on lab
2+
3+
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.
4+
5+
>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).
6+
7+
To start with, we'll go through [installing Puppet Development Kit](./hands-on-lab/01-installing-prereqs.md)(PDK).
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Install Puppet Development Kit (PDK) and other tools
2+
3+
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.
4+
5+
1. [Download PDK](https://puppet.com/download-puppet-development-kit) on your platform of choice.
6+
7+
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.
8+
9+
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.
10+
11+
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.
12+
13+
14+
## Next up
15+
16+
After installing the relevant tools, you'll [light up a few](./02-connecting-to-the-lightbulbs.md).
42.3 KB
Loading
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Connecting to the light bulbs
2+
3+
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.
4+
5+
## Hue Emulator
6+
7+
Use `java -jar` with the emulator's filename to run it, for example:
8+
9+
```
10+
david@davids:~$ java -jar ~/Downloads/HueEmulator-v0.8.jar
11+
```
12+
13+
It does not produce any output on the command line, but a window pops up with a hub and a few predefined lights:
14+
15+
![](./02-connecting-to-the-lightbulbs-emulator.png)
16+
17+
All you need now is to input a port (the default 8000 is usually fine) and click "Start" to activate the built-in server.
18+
19+
## Connecting to your hub
20+
21+
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).
22+
23+
24+
## Next up
25+
26+
Now that you have some lights up, you'll [create a module](./03-creating-a-new-module.md).
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Create a module
2+
3+
Depending on your preferences, you can use the VSCode/PDK integration or run PDK from the command line in a terminal of your choice.
4+
5+
## Create a module with VSCode
6+
7+
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:
8+
9+
![](./03-creating-a-new-module_vscode.png)
10+
11+
Click Enter (↩) to execute this and follow the on-screen prompts.
12+
13+
The module will open in a new VSCode window.
14+
15+
## Create a module from the command line
16+
17+
In your regular workspace (for example your home directory), run the following:
18+
19+
```
20+
pdk new module hue_workshop --skip-interview
21+
```
22+
23+
This command creates a new module `hue_workshop` in your directory of the same name, using all defaults. The output will look like:
24+
25+
```
26+
david@davids:~/tmp$ pdk new module hue_workshop --skip-interview
27+
pdk (INFO): Creating new module: hue_workshop
28+
pdk (INFO): Module 'hue_workshop' generated at path '/home/david/tmp/hue_workshop', from template 'file:///opt/puppetlabs/pdk/share/cache/pdk-templates.git'.
29+
pdk (INFO): In your module directory, add classes with the 'pdk new class' command.
30+
david@davids:~/tmp$ ls hue_workshop/
31+
appveyor.yml data files Gemfile.lock manifests Rakefile spec templates
32+
CHANGELOG.md examples Gemfile hiera.yaml metadata.json README.md tasks
33+
david@davids:~/tmp$
34+
```
35+
36+
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).
37+
38+
Open the new directory in your code editor:
39+
40+
```
41+
code -a hue_workshop
42+
```
43+
44+
45+
## Next up
46+
47+
Now that you have created a module, you'll [add a transport](./04-adding-a-new-transport.md).
21.7 KB
Loading
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Add a new transport
2+
3+
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:
4+
5+
Next, we'll active a few future defaults. In the `hue_workshop` directory, create a file called `.sync.yml` and paste the following:
6+
7+
```
8+
# .sync.yml
9+
---
10+
Gemfile:
11+
optional:
12+
':development':
13+
- gem: 'puppet-resource_api'
14+
- gem: 'faraday'
15+
- gem: 'rspec-json_expectations'
16+
spec/spec_helper.rb:
17+
mock_with: ':rspec'
18+
```
19+
20+
Run `pdk update` in the module's directory to deploy the changes in the module:
21+
22+
```
23+
david@davids:~/tmp/hue_workshop$ pdk update --force
24+
pdk (INFO): Updating david-hue_workshop using the default template, from 1.10.0 to 1.10.0
25+
26+
----------Files to be modified----------
27+
Gemfile
28+
spec/spec_helper.rb
29+
30+
----------------------------------------
31+
32+
You can find a report of differences in update_report.txt.
33+
34+
Do you want to continue and make these changes to your module? Yes
35+
36+
------------Update completed------------
37+
38+
2 files modified.
39+
40+
david@davids:~/tmp/hue_workshop$
41+
```
42+
43+
Then, create the actual transport:
44+
45+
```
46+
david@davids:~/tmp/hue$ pdk new transport hue
47+
pdk (INFO): Creating '/home/david/tmp/hue/lib/puppet/transport/hue.rb' from template.
48+
pdk (INFO): Creating '/home/david/tmp/hue/lib/puppet/transport/schema/hue.rb' from template.
49+
pdk (INFO): Creating '/home/david/tmp/hue/lib/puppet/util/network_device/hue/device.rb' from template.
50+
pdk (INFO): Creating '/home/david/tmp/hue/spec/unit/puppet/transport/hue_spec.rb' from template.
51+
pdk (INFO): Creating '/home/david/tmp/hue/spec/unit/puppet/transport/schema/hue_spec.rb' from template.
52+
david@davids:~/tmp/hue$
53+
```
54+
55+
## Checkpoint
56+
57+
To validate your new module and transport, run `pdk validate --parallel` and `pdk test unit`:
58+
59+
```
60+
david@davids:~/tmp/hue$ pdk validate --parallel
61+
pdk (INFO): Running all available validators...
62+
pdk (INFO): Using Ruby 2.5.5
63+
pdk (INFO): Using Puppet 6.4.2
64+
┌ [✔] Validating module using 5 threads ┌
65+
├──[✔] Checking metadata syntax (metadat├──son tasks/*.json).
66+
├──[✔] Checking task names (tasks/**/*).├──
67+
└──[✔] Checking YAML syntax (["**/*.yaml├──"*.yaml", "**/*.yml", "*.yml"]).
68+
└──[/] Checking module metadata style (metadata.json).
69+
└──[✔] Checking module metadata style (metadata.json).
70+
info: puppet-syntax: ./: Target does not contain any files to validate (**/*.pp).
71+
info: task-metadata-lint: ./: Target does not contain any files to validate (tasks/*.json).
72+
info: puppet-lint: ./: Target does not contain any files to validate (**/*.pp).
73+
david@davids:~/tmp/hue$ pdk test unit
74+
pdk (INFO): Using Ruby 2.5.5
75+
pdk (INFO): Using Puppet 6.4.2
76+
[✔] Preparing to run the unit tests.
77+
[✔] Running unit tests in parallel.
78+
Run options: exclude {:bolt=>true}
79+
Evaluated 6 tests in 2.405066937 seconds: 0 failures, 0 pending.
80+
david@davids:~/tmp/hue$
81+
```
82+
83+
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:
84+
85+
```
86+
david@davids:~/tmp/hue$ git init
87+
Initialized empty Git repository in ~/tmp/hue/.git/
88+
david@davids:~/tmp/hue$ git add -A
89+
david@davids:~/tmp/hue$ git commit -m 'initial commit'
90+
[master (root-commit) 67951dd] initial commit
91+
26 files changed, 887 insertions(+)
92+
create mode 100644 .fixtures.yml
93+
create mode 100644 .gitattributes
94+
create mode 100644 .gitignore
95+
create mode 100644 .gitlab-ci.yml
96+
create mode 100644 .pdkignore
97+
create mode 100644 .puppet-lint.rc
98+
create mode 100644 .rspec
99+
create mode 100644 .rubocop.yml
100+
create mode 100644 .sync.yml
101+
create mode 100644 .travis.yml
102+
create mode 100644 .yardopts
103+
create mode 100644 CHANGELOG.md
104+
create mode 100644 Gemfile
105+
create mode 100644 README.md
106+
create mode 100644 Rakefile
107+
create mode 100644 appveyor.yml
108+
create mode 100644 data/common.yaml
109+
create mode 100644 hiera.yaml
110+
create mode 100644 lib/puppet/transport/hue.rb
111+
create mode 100644 lib/puppet/transport/schema/hue.rb
112+
create mode 100644 lib/puppet/util/network_device/hue/device.rb
113+
create mode 100644 metadata.json
114+
create mode 100644 spec/default_facts.yml
115+
create mode 100644 spec/spec_helper.rb
116+
create mode 100644 spec/unit/puppet/transport/hue_spec.rb
117+
create mode 100644 spec/unit/puppet/transport/schema/hue_spec.rb
118+
david@davids:~/tmp/hue$
119+
```
120+
121+
## Next up
122+
123+
Now that you have everything ready, you'll [implement the transport](./05-implementing-the-transport.md).
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Implementing the transport - Exercise
2+
3+
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).
4+
5+
## Hints
6+
7+
* You can create a toggle option with the `Boolean` (`true` or `false`) data type. Add it to the `connection_info` in the transport schema.
8+
9+
* Make it an `Optional[Boolean]` so that users who do not require request debugging do not have to specify the value.
10+
11+
* To remember the value you passed, store `connection_info[:request_debug]` in a `@request_debug` variable.
12+
13+
* In the `hue_get` and `hue_put` methods, add `context.debug(message)` calls showing the method's arguments.
14+
15+
* Make the debugging optional based on your input by appending `if @request_debug` to each logging statement.
16+
17+
# Next Up
18+
19+
Now that the transport can talk to the remote target, it's time to [implement a provider](./06-implementing-the-provider.md).
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Implementing the transport
2+
3+
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.
4+
5+
## Schema
6+
7+
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`.
8+
9+
To connect to the HUE hub you need an IP address, a port, and an API key.
10+
11+
Replace the `connection_info` in `lib/puppet/transport/schema/hue.rb` with the following code:
12+
13+
```ruby
14+
connection_info: {
15+
host: {
16+
type: 'String',
17+
desc: 'The FQDN or IP address of the hue light system to connect to.',
18+
},
19+
port: {
20+
type: 'Optional[Integer]',
21+
desc: 'The port to use when connecting, defaults to 80.',
22+
},
23+
key: {
24+
type: 'String',
25+
desc: 'The access key that allows access to the hue light system.',
26+
sensitive: true,
27+
},
28+
},
29+
```
30+
31+
> 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.
32+
33+
34+
## Implementation
35+
36+
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.
37+
38+
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:
39+
40+
<!-- TODO: do we really need this? -- probably not ```
41+
# @summary
42+
# Expose the `Faraday` object connected to the hub
43+
attr_reader :connection
44+
45+
```-->
46+
```
47+
# @summary
48+
# Initializes and returns a faraday connection to the given host
49+
def initialize(_context, connection_info)
50+
# provide a default port
51+
port = connection_info[:port].nil? ? 80 : connection_info[:port]
52+
Puppet.debug "Connecting to #{connection_info[:host]}:#{port} with dev key"
53+
@connection = Faraday.new(url: "http://#{connection_info[:host]}:#{port}/api/#{connection_info[:key].unwrap}", ssl: { verify: false })
54+
end
55+
```
56+
57+
> Note the `unwrap` call on building the URL, to access the sensitive value.
58+
59+
### Facts
60+
61+
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:
62+
63+
Replace the example `facts` method in `lib/puppet/transport/hue.rb` with the following code:
64+
65+
```
66+
# @summary
67+
# Returns set facts regarding the HUE Hub
68+
def facts(_context)
69+
{ 'operatingsystem' => 'philips_hue' }
70+
end
71+
```
72+
73+
### Connection verification and closing
74+
75+
To enable better feedback when something goes wrong, a transport can implement a `verify` method to run extra checks on the credentials passed in.
76+
77+
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.
78+
79+
For this tutorial, replace the example methods with the following code:
80+
81+
```
82+
# @summary
83+
# Test that transport can talk to the remote target
84+
def verify(_context)
85+
end
86+
87+
# @summary
88+
# Close connection, free up resources
89+
def close(_context)
90+
@connection = nil
91+
end
92+
```
93+
94+
### Making requests
95+
96+
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.
97+
98+
Insert the following code after the `close` method:
99+
100+
```
101+
# @summary
102+
# Make a get request to the HUE Hub API
103+
def hue_get(context, url, args = nil)
104+
url = URI.escape(url) if url
105+
result = @connection.get(url, args)
106+
JSON.parse(result.body)
107+
rescue JSON::ParserError => e
108+
raise Puppet::ResourceError, "Unable to parse JSON response from HUE API: #{e}"
109+
end
110+
111+
# @summary
112+
# Sends an update command to the given url/connection
113+
def hue_put(context, url, message)
114+
message = message.to_json
115+
@connection.put(url, message)
116+
end
117+
```
118+
119+
## Exercise
120+
121+
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).
122+
123+
124+
# Next Up
125+
126+
Now that the transport can talk to the remote target, it's time to [implement a provider](./06-implementing-the-provider.md).

0 commit comments

Comments
 (0)