orchestrator_client 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []