orchestrator_client 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1b2b6d87e920753f666409e3a6c91b70913137c9
4
+ data.tar.gz: fbd13b48bf866358c5c11242176ed0ed319b667b
5
+ SHA512:
6
+ metadata.gz: f1c0eb3a348d3da0b56a29bc8457a9a98f7243d5f9c59930ef0a1edc1e0914d01bca0bbf01ce194d1b1b0495b486377478d5d5f15bbc33d1d1a07bab5afa9102
7
+ data.tar.gz: 4968053c6c727a79c960ce2756118572d9096675d2c3087bf59f38631f7c423c8c15b96a94821038b86c41ccdd2d1d53029977e1629d85bc412f4d4ddf73354a
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ *.gem
@@ -0,0 +1,46 @@
1
+ # How to contribute
2
+
3
+ * Make sure you have a [GitHub account](https://github.com/signup/free)
4
+ * Fork the repository on GitHub
5
+
6
+ ## Making Changes
7
+
8
+ * Create a topic branch from where you want to base your work (this is almost
9
+ definitely the master branch).
10
+ * To quickly create a topic branch based on master; `git branch
11
+ fix/master/my_contribution master` then checkout the new branch with `git
12
+ checkout fix/master/my_contribution`.
13
+ * Please avoid working directly on the
14
+ `master` branch.
15
+ * Make commits of logical units.
16
+ * Check for unnecessary whitespace with `git diff --check` before committing.
17
+ * Make sure your commit messages are in the proper format.
18
+
19
+ ````
20
+ Make the example in CONTRIBUTING imperative and concrete
21
+
22
+ Without this patch applied the example commit message in the CONTRIBUTING
23
+ document is not a concrete example. This is a problem because the
24
+ contributor is left to imagine what the commit message should look like
25
+ based on a description rather than an example. This patch fixes the
26
+ problem by making the example concrete and imperative.
27
+
28
+ The first line is a real life imperative statement with a ticket number
29
+ from our issue tracker. The body describes the behavior without the patch,
30
+ why this is a problem, and how the patch fixes the problem when applied.
31
+ ````
32
+
33
+ * Make sure you have added the necessary tests for your changes.
34
+ * Run _all_ the tests to assure nothing else was accidentally broken.
35
+
36
+ ## Submitting Changes
37
+
38
+ * Sign the [Contributor License Agreement](http://links.puppet.com/cla).
39
+ * Push your changes to a topic branch in your fork of the repository.
40
+ * Submit a pull request to the repository in the puppetlabs organization.
41
+
42
+ # Additional Resources
43
+
44
+ * [Contributor License Agreement](http://links.puppet.com/cla)
45
+ * [General GitHub documentation](http://help.github.com/)
46
+ * [GitHub pull request documentation](http://help.github.com/send-pull-requests/)
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'rspec'
6
+ gem 'webmock'
7
+
8
+ gem 'pry'
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ orchestrator_api
2
+
3
+ Copyright (C) 2016 Puppet Labs Inc
4
+
5
+ Puppet,Inc. can be contacted at: info@puppet.com
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,75 @@
1
+ # OrchestratorClient
2
+
3
+ A simple client for interacting with the Orchestration Services API in Puppet Enterprise
4
+ [Puppet orchestration API](https://docs.puppet.com/pe/latest/api_index.html#puppet-orchestrator-api)
5
+
6
+ ## Compatibility
7
+
8
+ Currently, this client supports the "V1" endpoints shipped as part of Puppet Enterprise 2016.2.
9
+
10
+ ## Installation
11
+
12
+ ```shell
13
+ gem install orchestrator_client
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ Requires a token with 'Orchestration' permissions. By default the token is
19
+ expected to be at `~/.puppetlabs/token` which is the default location used by
20
+ `puppet-access` when creating token.
21
+
22
+ ### initialization Settings
23
+
24
+ * `service-url` **[required]** - Base URL for the location of the Orchestrator API service
25
+ * `ca_cert` **[required]** - Path to the CA certificate file needed to verify the SSL connection to the API.
26
+ * `token_path`- Path to a file with the RBAC token in it (defaults to `~/.puppetlabs/token`)
27
+ * `token` - Pass directly the RBAC token, if specified the token will be used instead of a token from file.
28
+
29
+ ### Example
30
+
31
+ ```ruby
32
+ require 'orchestrator_client'
33
+
34
+ # Create a new client
35
+ # Requires at least a server name and path to the CA certificate
36
+
37
+ client = OrchestratorClient.new({
38
+ 'service-url' => 'https://orchestrator.example.lan:8143/orchestrator/v1',
39
+ 'ca_cert' => '/path/to/cert'
40
+ })
41
+
42
+ ## Access endpoints through the client object
43
+
44
+ # Get details on all known jobs
45
+ result = client.jobs.all
46
+
47
+ # Get details on Individual jobs (job "5" in this example)
48
+ client.jobs.details(5)
49
+
50
+ # Perform an orchestrator deployment
51
+ new_job_details = client.command.deploy('production', {'noop' => true })
52
+ ```
53
+
54
+ ## Tests
55
+
56
+ ```shell
57
+ bundle install
58
+ bundle exec rspec
59
+ ```
60
+
61
+ ## Issues & Contributions
62
+
63
+ File issues or feature requests using [GitHub
64
+ issues](https://github.com/puppetlabs/orchestrator_api-ruby/issues).
65
+
66
+ If you are interested in contributing to this project, please see the
67
+ [Contribution Guidelines](CONTRIBUTING.md)
68
+
69
+ ## Authors
70
+
71
+ Tom Linkin <tom@puppet.com>
72
+
73
+ ## License
74
+
75
+ See LICENSE.
@@ -0,0 +1,87 @@
1
+ require 'net/https'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'openssl'
5
+
6
+ class OrchestratorClient
7
+ require 'orchestrator_client/error'
8
+ require 'orchestrator_client/command'
9
+ require 'orchestrator_client/jobs'
10
+ require 'orchestrator_client/job'
11
+ require 'orchestrator_client/config'
12
+
13
+ attr_accessor :config
14
+
15
+ def initialize(overrides, load_files=false)
16
+ @config = Config.new(overrides, load_files)
17
+ @config.validate
18
+ end
19
+
20
+ def make_uri(path)
21
+ URI.parse("#{config.root_url}#{path}")
22
+ end
23
+
24
+ def create_http(uri)
25
+ http = Net::HTTP.new(uri.host, uri.port)
26
+ http.use_ssl = true
27
+ http.ssl_version = :TLSv1
28
+ http.ca_file = config['cacert']
29
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
30
+ http
31
+ end
32
+
33
+ def get(location)
34
+ uri = make_uri(location)
35
+ https = create_http(uri)
36
+ req = Net::HTTP::Get.new(uri)
37
+ req['Content-Type'] = "application/json"
38
+ req.add_field('X-Authentication', @config.token)
39
+ res = https.request(req)
40
+
41
+ if res.code != "200"
42
+ raise OrchestratorClient::ApiError.make_error_from_response(res)
43
+ end
44
+
45
+ JSON.parse(res.body)
46
+ end
47
+
48
+ def post(location, body)
49
+ uri = make_uri(location)
50
+ https = create_http(uri)
51
+
52
+ req = Net::HTTP::Post.new(uri)
53
+ req['Content-Type'] = "application/json"
54
+ req.add_field('X-Authentication', @config.token)
55
+ req.body = body.to_json
56
+ res = https.request(req)
57
+
58
+ if res.code != "202"
59
+ raise OrchestratorClient::ApiError.make_error_from_response(res)
60
+ end
61
+
62
+ JSON.parse(res.body)
63
+ end
64
+
65
+ def command
66
+ @command ||= OrchestratorClient::Command.new(self)
67
+ end
68
+
69
+ def jobs
70
+ @jobs ||= OrchestratorClient::Jobs.new(self)
71
+ end
72
+
73
+ def new_job(options, type = :deploy)
74
+ OrchestratorClient::Job.new(self, options, type)
75
+ end
76
+
77
+ def run_task(options)
78
+ job = OrchestratorClient::Job.new(self, options, :task)
79
+ job.start
80
+ job.wait
81
+ job.nodes['items']
82
+ end
83
+
84
+ def root
85
+ get(url)
86
+ end
87
+ end
@@ -0,0 +1,21 @@
1
+ class OrchestratorClient::Command
2
+
3
+ def initialize(https)
4
+ @https = https
5
+ end
6
+
7
+ def task(options = {})
8
+ raise ArgumentError, 'Must pass options as a hash' unless options.is_a? Hash
9
+ @https.post("command/task", options)
10
+ end
11
+
12
+ def deploy(options = {})
13
+ raise ArgumentError, 'Must pass options as a hash' unless options.is_a? Hash
14
+ @https.post("command/deploy", options)
15
+ end
16
+
17
+ def stop(job_number)
18
+ data = {"job" => "#{job_number}"}
19
+ @https.post("command/stop",data)
20
+ end
21
+ end
@@ -0,0 +1,96 @@
1
+ require 'json'
2
+
3
+ class OrchestratorClient::Config
4
+
5
+ def initialize(overrides = nil, load_files=false)
6
+ @overrides = overrides || {}
7
+ @load_files = load_files
8
+ end
9
+
10
+ def load_file(path)
11
+ File.open(path) {|f| JSON.parse(f.read)['options']}
12
+ end
13
+
14
+ def puppetlabs_root
15
+ "/etc/puppetlabs"
16
+ end
17
+
18
+ def global_conf
19
+ File.join(puppetlabs_root, 'client-tools', 'orchestrator.conf')
20
+ end
21
+
22
+ def user_root
23
+ File.join(Dir.home, '.puppetlabs')
24
+ end
25
+
26
+ def user_conf
27
+ File.join(user_root, 'client-tools', 'orchestrator.conf')
28
+ end
29
+
30
+ def cacert
31
+ "#{puppetlabs_root}/puppet/ssl/certs/ca.pem"
32
+ end
33
+
34
+ def defaults
35
+ { 'cacert' => cacert,
36
+ 'token-file' => File.join(user_root, 'token'),
37
+ }
38
+ end
39
+
40
+ def load_config
41
+ config = defaults
42
+ if @load_files
43
+ if File.exists?(global_conf) && file.readable?(global_conf)
44
+ config = config.merge(load_file(global_conf))
45
+ end
46
+
47
+ if @overrides['config-file']
48
+ config = config.merge(load_file(@overrides['config-file']))
49
+ elsif File.exists?(user_conf) && File.readable?(user_conf)
50
+ config = config.merge(load_file(user_conf))
51
+ end
52
+ end
53
+
54
+ config.merge(@overrides)
55
+ end
56
+
57
+ def validate
58
+ if config['service-url'].nil?
59
+ raise OrchestratorClient::ConfigError.new("'service-url' is required in config")
60
+ end
61
+
62
+ if config['cacert'].nil?
63
+ raise OrchestratorClient::ConfigError.new("'cacert' is required in config")
64
+ end
65
+ end
66
+
67
+ def config
68
+ @config ||= load_config
69
+ end
70
+
71
+ def overrides_only
72
+ @config = @overrides
73
+ end
74
+
75
+ def load_token
76
+ @config['token'] || File.open(config['token-file']) { |f| f.read }
77
+ end
78
+
79
+ def token
80
+ @token ||= load_token
81
+ end
82
+
83
+ def root_url
84
+ unless @root_url
85
+ url = @config['service-url']
86
+ url += '/' if url !~ /\/$/
87
+ url += 'orchestrator/v1/'
88
+ @root_url = url
89
+ end
90
+ @root_url
91
+ end
92
+
93
+ def [](key)
94
+ @config[key]
95
+ end
96
+ end
@@ -0,0 +1,59 @@
1
+
2
+ class OrchestratorClient::ConfigError < RuntimeError
3
+ end
4
+
5
+ class OrchestratorClient::ApiError < RuntimeError
6
+
7
+ def initialize(data,code)
8
+ @code = code
9
+ @kind = data['kind']
10
+ @details = data['details']
11
+ super(data['msg'])
12
+ end
13
+
14
+ def self.make_error_from_response(res)
15
+ begin
16
+ data = JSON.parse(res.body)
17
+ rescue
18
+ return OrchestratorClient::BadResponse.new("Response body was not valid JSON: #{res.body}")
19
+ end
20
+ code = res.code
21
+
22
+ case data['kind']
23
+ when 'puppetlabs.validators/validation-error'
24
+ ValidationError.new(data, code)
25
+ when 'puppetlabs.orchestrator/unknown-job'
26
+ UnknownJob.new(data, code)
27
+ when 'puppetlabs.orchestrator/unknown-environment'
28
+ UnknownEnvironment.new(data, code)
29
+ when 'puppetlabs.orchestrator/empty-environment'
30
+ EmptyEnvironment.new(data, code)
31
+ when 'puppetlabs.orchestrator/empty-target'
32
+ EmptyTarget.new(data, code)
33
+ when 'puppetlabs.orchestrator/dependency-cycle'
34
+ DependencyCycle.new(data, code)
35
+ when 'puppetlabs.orchestrator/puppetdb-error'
36
+ PuppetdbError.new(data, code)
37
+ when 'puppetlabs.orchestrator/query-error'
38
+ QueryError.new(data, code)
39
+ when 'puppetlabs.orchestrator/unknown-error'
40
+ UnknownError.new(data, code)
41
+ when 'puppetlabs.orchestrator/not-permitted'
42
+ UnauthorizedError.new(data, code)
43
+ else
44
+ OrchestratorClient::ApiError.new(data, code)
45
+ end
46
+ end
47
+ end
48
+
49
+ class OrchestratorClient::ApiError::ValidationError < OrchestratorClient::ApiError; end
50
+ class OrchestratorClient::ApiError::UnknownJob < OrchestratorClient::ApiError; end
51
+ class OrchestratorClient::ApiError::UnknownEnvironment < OrchestratorClient::ApiError; end
52
+ class OrchestratorClient::ApiError::EmptyEnvironment < OrchestratorClient::ApiError; end
53
+ class OrchestratorClient::ApiError::EmptyTarget < OrchestratorClient::ApiError; end
54
+ class OrchestratorClient::ApiError::DependencyCycle < OrchestratorClient::ApiError; end
55
+ class OrchestratorClient::ApiError::PuppetdbError < OrchestratorClient::ApiError; end
56
+ class OrchestratorClient::ApiError::QueryError < OrchestratorClient::ApiError; end
57
+ class OrchestratorClient::ApiError::UnknownError < OrchestratorClient::ApiError; end
58
+ class OrchestratorClient::ApiError::UnauthorizedError < OrchestratorClient::ApiError; end
59
+ class OrchestratorClient::BadResponse < RuntimeError; end
@@ -0,0 +1,93 @@
1
+ class OrchestratorClient::Job
2
+
3
+ DONE_STATES = ['stopped', 'finished', 'failed']
4
+ DONE_EVENTS = ['job_aborted', 'job_finished']
5
+
6
+ attr_accessor :job_name, :options, :job_id
7
+
8
+ def validate_scope
9
+ scope = @options['scope']
10
+ if scope.empty
11
+ Raise ArgumentError 'Scope cannot be empty'
12
+ elif scope['whole_environment']
13
+ puts 'Deprecation Warning: Whole environment behavior may not be stable'
14
+ end
15
+ end
16
+
17
+ def initialize(client, options = {}, type=:deploy)
18
+ @client = client
19
+ @options = options
20
+ @type = type
21
+ end
22
+
23
+ def start
24
+ case @type
25
+ when :deploy
26
+ result = @client.command.deploy(options)
27
+ when :task
28
+ result = @client.command.task(options)
29
+ end
30
+
31
+ @job_name = result['job']['name']
32
+ @job_id = result['job']['id']
33
+ @next_event=nil
34
+ @job_name
35
+ end
36
+
37
+ def stop
38
+ unless job_name
39
+ Raise ArgumentError "Job name not known to stop"
40
+ end
41
+ end
42
+
43
+ def assert_started?
44
+ Raise ArgumentError "Job is not yet started" unless @job_name
45
+ end
46
+
47
+ def get_details
48
+ assert_started?
49
+ @details = @client.jobs.details(@job_name)
50
+ end
51
+
52
+ def details
53
+ @details ||= get_details
54
+ end
55
+
56
+ def report
57
+ @client.jobs.report(@job_name)
58
+ end
59
+
60
+ def nodes
61
+ @client.jobs.nodes(@job_name)
62
+ end
63
+
64
+ # A poll the events endpoint yielding each event
65
+ def each_event
66
+ finished = false
67
+ start = nil
68
+ while !finished
69
+ events = @client.jobs.events(@job_name)
70
+ start = events['next-events']['event']
71
+ if events['items'].empty?
72
+ sleep 1
73
+ else
74
+ events['items'].each do |event|
75
+ finished = true if DONE_EVENTS.include?(event['type'])
76
+ yield event
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def wait(timeout=1000)
83
+ counter = 0
84
+ while counter < timeout
85
+ get_details
86
+ if DONE_STATES.include?(details['state'])
87
+ return report
88
+ end
89
+ sleep 1
90
+ counter += 1
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,36 @@
1
+ class OrchestratorClient::Jobs
2
+
3
+ def initialize(http)
4
+ @https = http
5
+ end
6
+
7
+ def all(limit=nil)
8
+ url = "jobs"
9
+ if limit
10
+ url << "?limit=#{limit}"
11
+ end
12
+
13
+ @https.get(url)
14
+ end
15
+
16
+ def details(id)
17
+ @https.get("jobs/#{id}")
18
+ end
19
+
20
+ def nodes(id)
21
+ @https.get("jobs/#{id}/nodes")
22
+ end
23
+
24
+ def report(id)
25
+ @https.get("jobs/#{id}/report")
26
+ end
27
+
28
+ def events(id, start = nil)
29
+ url = "jobs/#{id}/events"
30
+ if start
31
+ url << "?start=#{start}"
32
+ end
33
+
34
+ @https.get(url)
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ class OrchestratorClient
2
+ VERSION = '0.2.1'.freeze
3
+ end
@@ -0,0 +1,17 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'orchestrator_client/version'
5
+ Gem::Specification.new do |s|
6
+ s.name = 'orchestrator_client'
7
+ s.version = OrchestratorClient::VERSION
8
+ s.summary = "Simple Ruby client library for PE Orchestration Services"
9
+ s.authors = "Puppet"
10
+ s.email = 'info@puppetlabs.com'
11
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
12
+ f.match(%r{^(spec|scripts)/})
13
+ end
14
+ s.homepage = 'https://github.com/puppetlabs/ruby-orchestrator_api'
15
+ s.license = "Apache-2.0"
16
+ s.require_paths = ["lib"]
17
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: orchestrator_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Puppet
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-04 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: info@puppetlabs.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".gitignore"
20
+ - CONTRIBUTING.md
21
+ - Gemfile
22
+ - LICENSE
23
+ - README.md
24
+ - lib/orchestrator_client.rb
25
+ - lib/orchestrator_client/command.rb
26
+ - lib/orchestrator_client/config.rb
27
+ - lib/orchestrator_client/error.rb
28
+ - lib/orchestrator_client/job.rb
29
+ - lib/orchestrator_client/jobs.rb
30
+ - lib/orchestrator_client/version.rb
31
+ - orchestrator_client.gemspec
32
+ homepage: https://github.com/puppetlabs/ruby-orchestrator_api
33
+ licenses:
34
+ - Apache-2.0
35
+ metadata: {}
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubyforge_project:
52
+ rubygems_version: 2.6.12
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: Simple Ruby client library for PE Orchestration Services
56
+ test_files: []