linodeapi 1.0.1 → 2.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d2b6266cebf4ba879b7b79560958046dcfbeef85
4
- data.tar.gz: e4f7be859b3b424d4023d228a2bf465652b27c8d
3
+ metadata.gz: 386eb467c95101a915f20e0859bf356bb8c765ed
4
+ data.tar.gz: 45437aba6937cb0e0a03de655948479d5938241d
5
5
  SHA512:
6
- metadata.gz: 9c0400892e248bf04836685ea695ed2415dd5c38b39fd12f55028685a178d3cc6891ac99c07a6c730b2ada07c19058a86dad965bfc3ad472c19a7cb0df65586c
7
- data.tar.gz: a3645671fea6219314385c06f922eb957796460beb4f864a8df72700e2210d306dd699d39c00079791fb7b3546256284d6337b386180b3f2d942642bfe3bd1a6
6
+ metadata.gz: 18391de6e8204d94eef64f38b28a6f5fba0e0c3f37bb948029237cdeebfbbddcd00df70e107c88450c14e6700f2c1cb9b1c257d968cd5d6b87586dd27cfff1ed
7
+ data.tar.gz: a01e3df48784a83b12a8cabf96e0608505c825d56a7fd4a81a7ebd330fd41d18a26ec1d7134df3b69a23cc0191c0d37e89f0e5b294c93b3f34e35df9618569f5
@@ -0,0 +1,3 @@
1
+ 2.4.2
2
+ 2.3.5
3
+ 2.2.8
@@ -1,31 +1,25 @@
1
- require 'yaml'
1
+ Prospectus.extra_dep('file', 'prospectus_circleci')
2
2
 
3
- item do
4
- expected do
5
- static
6
- set 'green'
7
- end
3
+ my_slug = 'akerl/linodeapi'
8
4
 
9
- actual do
10
- gemnasium
11
- slug 'akerl/linodeapi'
12
- end
5
+ item do
6
+ noop
13
7
 
14
8
  deps do
15
- YAML.load(`./dev/cache_spec.rb --noop`).each do |item_name, (old, new)|
16
- item do
17
- name "api.spec.#{item_name}"
9
+ item do
10
+ name 'gems'
18
11
 
19
- expected do
20
- static
21
- set new
22
- end
12
+ expected do
13
+ static
14
+ set 'green'
15
+ end
23
16
 
24
- actual do
25
- static
26
- set old
27
- end
17
+ actual do
18
+ gemnasium
19
+ slug my_slug
28
20
  end
29
21
  end
30
22
  end
23
+
24
+ extend ProspectusCircleci::Build.new(my_slug)
31
25
  end
@@ -1,2 +1,5 @@
1
- Encoding:
2
- Enabled: false
1
+ inherit_gem:
2
+ goodcop: .rubocop.yml
3
+ AllCops:
4
+ Include:
5
+ - 'dev/*.rb'
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
-
data/README.md CHANGED
@@ -3,7 +3,7 @@ linodeapi
3
3
 
4
4
  [![Gem Version](https://img.shields.io/gem/v/linodeapi.svg)](https://rubygems.org/gems/linodeapi)
5
5
  [![Dependency Status](https://img.shields.io/gemnasium/akerl/linodeapi.svg)](https://gemnasium.com/akerl/linodeapi)
6
- [![Build Status](https://img.shields.io/circleci/project/akerl/linodeapi.svg)](https://circleci.com/gh/akerl/linodeapi)
6
+ [![Build Status](https://img.shields.io/circleci/project/akerl/linodeapi/master.svg)](https://circleci.com/gh/akerl/linodeapi)
7
7
  [![Coverage Status](https://img.shields.io/codecov/c/github/akerl/linodeapi.svg)](https://codecov.io/github/akerl/linodeapi)
8
8
  [![Code Quality](https://img.shields.io/codacy/60b77ea8214241b5ac970bcdfd584587.svg)](https://www.codacy.com/app/akerl/linodeapi)
9
9
  [![MIT Licensed](https://img.shields.io/badge/license-MIT-green.svg)](https://tldrlegal.com/license/mit-license)
@@ -54,6 +54,12 @@ api.linode.create
54
54
  # ArgumentError: linode.create requires planid
55
55
  ```
56
56
 
57
+ ### Retryable helper
58
+
59
+ The LinodeAPI::Retryable class automatically handles 429 errors (really, any HTTP error that is accompanied by a Retry-After header).
60
+
61
+ This class behaves identially to the LinodeAPI::Raw class, with 2 additional parameters at creation: `max_retries` sets the total number of requests it will attempt per call and `max_delay` sets the maximum delay between attempts. They default to 3 tries and 60 seconds, respectively.
62
+
57
63
  ## Installation
58
64
 
59
65
  gem install linodeapi
data/Rakefile CHANGED
@@ -7,8 +7,7 @@ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  desc 'Run Rubocop on the gem'
9
9
  RuboCop::RakeTask.new(:rubocop) do |task|
10
- task.patterns = ['lib/**/*.rb', 'spec/**/*.rb', 'dev/*.rb']
11
10
  task.fail_on_error = true
12
11
  end
13
12
 
14
- task default: [:spec, :rubocop, :build, :install]
13
+ task default: %i[spec rubocop build install]
data/circle.yml CHANGED
@@ -1,12 +1,7 @@
1
1
  dependencies:
2
2
  override:
3
- - 'rvm-exec 1.9.3-p551 bundle install'
4
- - 'rvm-exec 2.0.0-p645 bundle install'
5
- - 'rvm-exec 2.1.6 bundle install'
6
- - 'rvm-exec 2.2.2 bundle install'
3
+ - 'for i in $(cat .circle-ruby) ; do rvm-exec $i gem update --system --no-doc || exit 1 ; done'
4
+ - 'for i in $(cat .circle-ruby) ; do rvm-exec $i bundle install || exit 1 ; done'
7
5
  test:
8
6
  override:
9
- - 'rvm-exec 1.9.3-p551 bundle exec rake'
10
- - 'rvm-exec 2.0.0-p645 bundle exec rake'
11
- - 'rvm-exec 2.1.6 bundle exec rake'
12
- - 'rvm-exec 2.2.2 bundle exec rake'
7
+ - 'for i in $(cat .circle-ruby) ; do rvm-exec $i bundle exec rake || exit 1 ; done'
@@ -686,39 +686,10 @@ linode:
686
686
  create:
687
687
  - desc: ''
688
688
  - params:
689
- - fromdistributionid:
690
- - desc: ''
691
- - required: false
692
- - type: :numeric
693
- - isreadonly:
694
- - desc: Enable forced read-only for this Disk
695
- - required: false
696
- - type: :boolean
697
- - label:
698
- - desc: The display label for this Disk
699
- - required: true
700
- - type: :string
701
689
  - linodeid:
702
690
  - desc: ''
703
691
  - required: true
704
692
  - type: :numeric
705
- - rootpass:
706
- - desc: ''
707
- - required: false
708
- - type: :string
709
- - rootsshkey:
710
- - desc: ''
711
- - required: false
712
- - type: :string
713
- - size:
714
- - desc: The size in MB of this Disk.
715
- - required: true
716
- - type: :numeric
717
- - type:
718
- - desc: 'The formatted type of this disk. Valid types are: ext3, ext4, swap,
719
- raw'
720
- - required: true
721
- - type: :string
722
693
  - throws:
723
694
  - NOTFOUND
724
695
  - VALIDATION
@@ -912,14 +883,6 @@ linode:
912
883
  - desc: ''
913
884
  - required: true
914
885
  - type: :numeric
915
- - isreadonly:
916
- - desc: Enable forced read-only for this Disk
917
- - required: false
918
- - type: :boolean
919
- - label:
920
- - desc: The display label for this Disk
921
- - required: false
922
- - type: :string
923
886
  - linodeid:
924
887
  - desc: ''
925
888
  - required: false
@@ -1016,6 +979,16 @@ linode:
1016
979
  - required: false
1017
980
  - type: :boolean
1018
981
  - throws: []
982
+ kvmify:
983
+ - desc: Changes a Linode's hypervisor from Xen to KVM.
984
+ - params:
985
+ - linodeid:
986
+ - desc: The LinodeID to migrate to KVM.
987
+ - required: true
988
+ - type: :numeric
989
+ - throws:
990
+ - NOTFOUND
991
+ - VALIDATION
1019
992
  list:
1020
993
  - desc: 'Returns a list of all Linodes user has access or delete to, including some
1021
994
  properties. Status values are -1: Being Created, 0: Brand New, 1: Running,
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'httparty'
3
4
 
4
5
  ##
@@ -74,4 +75,6 @@ module LinodeAPI
74
75
  end
75
76
  end
76
77
 
78
+ require 'linodeapi/errors'
77
79
  require 'linodeapi/raw'
80
+ require 'linodeapi/retryable'
@@ -0,0 +1,47 @@
1
+ module LinodeAPI
2
+ ##
3
+ # A standard HTTP error with an embedded error code
4
+ class HTTPError < StandardError
5
+ attr_reader :code
6
+
7
+ def initialize(code, msg = 'HTTP Error encountered')
8
+ @code = code
9
+ super(msg)
10
+ end
11
+ end
12
+
13
+ ##
14
+ # A retryable error that has exceeded its max retries
15
+ class RetriedHTTPError < HTTPError
16
+ attr_reader :retries
17
+
18
+ def initialize(code, retries, msg = nil)
19
+ @retries = retries
20
+ msg ||= "HTTP Error encountered (retried #{retries} times)"
21
+ super(code, msg)
22
+ end
23
+ end
24
+
25
+ ##
26
+ # A retryable API error with embedded code and requested delay
27
+ class RetryableHTTPError < HTTPError
28
+ attr_reader :delay
29
+
30
+ def initialize(code, delay, msg = 'Retryable HTTP Error encountered')
31
+ @delay = delay.to_i
32
+ super(code, msg)
33
+ end
34
+ end
35
+
36
+ ##
37
+ # An API error in the body of the HTTP response
38
+ class APIError < StandardError
39
+ attr_reader :action, :details
40
+
41
+ def initialize(resp, msg = 'API Error encountered')
42
+ @action = resp['ACTION']
43
+ @details = resp['ERRORARRAY']
44
+ super(msg)
45
+ end
46
+ end
47
+ end
@@ -9,17 +9,16 @@ module LinodeAPI
9
9
  class Raw
10
10
  include HTTParty
11
11
 
12
- attr_reader :apikey, :spec, :names
13
-
14
12
  def initialize(params = {})
13
+ @options = params
15
14
  self.class.base_uri params.fetch(:endpoint, DEFAULT_ENDPOINT)
16
- @names = params.fetch(:names) { [] }
17
- @spec = params.fetch(:spec) { LinodeAPI.spec }
18
- @apikey = params.fetch(:apikey) { authenticate(params) }
15
+ @options[:names] ||= []
16
+ @options[:spec] ||= LinodeAPI.spec
17
+ @options[:apikey] ||= authenticate(params)
19
18
  end
20
19
 
21
- def respond_to?(method, include_private = false)
22
- super || @spec[:subs].include?(method)
20
+ def respond_to_missing?(method, include_private = false)
21
+ spec[:subs].include?(method) || super
23
22
  end
24
23
 
25
24
  def to_s
@@ -27,10 +26,22 @@ module LinodeAPI
27
26
  end
28
27
  alias inspect to_s
29
28
 
29
+ def names
30
+ @names ||= @options[:names]
31
+ end
32
+
33
+ def spec
34
+ @spec ||= @options[:spec]
35
+ end
36
+
37
+ def apikey
38
+ @apikey ||= @options[:apikey]
39
+ end
40
+
30
41
  private
31
42
 
32
43
  def authenticate(params = {})
33
- return [] unless @names.empty?
44
+ return [] unless names.empty?
34
45
  unless (params.values_at :username, :password).all?
35
46
  raise ArgumentError, 'You must provide either an API key or user/pass'
36
47
  end
@@ -39,18 +50,18 @@ module LinodeAPI
39
50
 
40
51
  def method_missing(method, *args, &block)
41
52
  return super unless respond_to? method
42
- case @spec[:subs][method][:type]
53
+ case spec[:subs][method][:type]
43
54
  when :group then make_group method
44
55
  when :call then make_call method, *args
45
56
  end
46
57
  end
47
58
 
48
59
  def make_group(method)
49
- group = Raw.new(
50
- spec: @spec[:subs][method],
51
- apikey: @apikey,
52
- username: @username,
53
- names: @names + [method]
60
+ group = self.class.new(
61
+ @options.dup.merge(
62
+ spec: spec[:subs][method],
63
+ names: names + [method]
64
+ )
54
65
  )
55
66
  name = "@#{method}".to_sym
56
67
  instance_variable_set name, group
@@ -64,28 +75,34 @@ module LinodeAPI
64
75
  end
65
76
 
66
77
  def call(method, params = {})
67
- spec = @spec[:subs][method]
68
- method = (@names + [method.to_s]).join '.'
69
- options = self.class.validate method, spec[:params], params
70
- options[:api_key] = @apikey
78
+ mspec = spec[:subs][method]
79
+ method = (names + [method.to_s]).join '.'
80
+ options = self.class.validate method, mspec[:params], params
81
+ options[:api_key] = apikey
71
82
  options[:api_action] = method
72
83
  error_check self.class.post('', body: options)
73
84
  end
74
85
 
75
86
  def error_check(resp)
76
- code = resp.code
77
- raise("API threw HTTP error code #{code}") unless code == 200
87
+ error = create_http_error(resp)
88
+ raise(error) if error
78
89
  data = resp.parsed_response
79
90
  raise('Invalid API response received') if data.nil?
80
91
  self.class.parse data
81
92
  end
82
93
 
94
+ def create_http_error(resp)
95
+ code = resp.code
96
+ return nil if code == 200
97
+ delay = resp.headers['Retry-After']
98
+ return RetryableHTTPError.new(code, delay) if delay
99
+ HTTPError.new(code)
100
+ end
101
+
83
102
  class << self
84
103
  def parse(resp)
85
104
  resp['ERRORARRAY'].reject! { |x| x['ERRORCODE'].zero? }
86
- unless resp['ERRORARRAY'].empty?
87
- raise "API Error on #{resp['ACTION']}: #{resp['ERRORARRAY']}"
88
- end
105
+ raise(APIError, resp) unless resp['ERRORARRAY'].empty?
89
106
  data = resp['DATA']
90
107
  data.is_a?(Hash) ? clean(data) : data.map { |x| clean x }
91
108
  end
@@ -94,8 +111,8 @@ module LinodeAPI
94
111
  OpenStruct.new(Hash[object.map { |k, v| [k.downcase.to_sym, v] }])
95
112
  end
96
113
 
97
- def validate(method, spec, given)
98
- spec.each_with_object({}) do |(param, info), options|
114
+ def validate(method, mspec, given)
115
+ mspec.each_with_object({}) do |(param, info), options|
99
116
  if given.include? param
100
117
  options[param] = VALIDATION_METHODS[info[:type]].call given[param]
101
118
  elsif info[:required]
@@ -0,0 +1,36 @@
1
+ module LinodeAPI
2
+ ##
3
+ # A wrapper for the Raw API that can retry retryable errors
4
+ class Retryable < Raw
5
+ def initialize(params = {})
6
+ super
7
+ @options[:max_retries] ||= 3
8
+ @options[:max_delay] ||= 60
9
+ end
10
+
11
+ private
12
+
13
+ alias single_call call
14
+
15
+ def call(*args)
16
+ call_with_retries(max_retries, *args)
17
+ end
18
+
19
+ def call_with_retries(remaining, *args)
20
+ single_call(*args)
21
+ rescue RetryableHTTPError => e
22
+ raise(RetriedHTTPError.new(e.code, max_retries)) if remaining < 0
23
+ delay = [e.delay, max_delay].min
24
+ sleep delay
25
+ call_with_retries(remaining - 1, *args)
26
+ end
27
+
28
+ def max_retries
29
+ @options[:max_retries]
30
+ end
31
+
32
+ def max_delay
33
+ @options[:max_delay]
34
+ end
35
+ end
36
+ end
@@ -1,10 +1,10 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'linodeapi'
3
- s.version = '1.0.1'
4
- s.date = Time.now.strftime("%Y-%m-%d")
3
+ s.version = '2.0.0'
4
+ s.date = Time.now.strftime('%Y-%m-%d')
5
5
 
6
6
  s.summary = 'Linode API wrapper'
7
- s.description = "Wraps the Linode API with multiple levels of interaction"
7
+ s.description = 'Wraps the Linode API with multiple levels of interaction'
8
8
  s.authors = ['Les Aker']
9
9
  s.email = 'me@lesaker.org'
10
10
  s.homepage = 'https://github.com/akerl/linodeapi'
@@ -13,13 +13,14 @@ Gem::Specification.new do |s|
13
13
  s.files = `git ls-files`.split
14
14
  s.test_files = `git ls-files spec/*`.split
15
15
 
16
- s.add_dependency 'httparty', '~> 0.13.1'
16
+ s.add_dependency 'httparty', '~> 0.15.5'
17
17
 
18
- s.add_development_dependency 'rubocop', '~> 0.41.1'
19
- s.add_development_dependency 'rake', '~> 11.2.0'
18
+ s.add_development_dependency 'rubocop', '~> 0.50.0'
19
+ s.add_development_dependency 'goodcop', '~> 0.1.0'
20
+ s.add_development_dependency 'rake', '~> 12.1.0'
20
21
  s.add_development_dependency 'codecov', '~> 0.1.1'
21
- s.add_development_dependency 'rspec', '~> 3.5.0'
22
- s.add_development_dependency 'fuubar', '~> 2.0.0'
23
- s.add_development_dependency 'webmock', '~> 2.1.0'
22
+ s.add_development_dependency 'rspec', '~> 3.6.0'
23
+ s.add_development_dependency 'fuubar', '~> 2.2.0'
24
+ s.add_development_dependency 'webmock', '~> 3.0.0'
24
25
  s.add_development_dependency 'vcr', '~> 3.0.0'
25
26
  end
@@ -19,7 +19,7 @@ describe LinodeAPI::Raw do
19
19
  VCR.use_cassette('basic_auth_fail') do
20
20
  expect do
21
21
  LinodeAPI::Raw.new(username: 'bad', password: 'bad')
22
- end.to raise_error RuntimeError
22
+ end.to raise_error LinodeAPI::APIError
23
23
  end
24
24
  end
25
25
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linodeapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Les Aker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-01 00:00:00.000000000 Z
11
+ date: 2017-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -16,42 +16,56 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.13.1
19
+ version: 0.15.5
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.13.1
26
+ version: 0.15.5
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rubocop
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.41.1
33
+ version: 0.50.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.41.1
40
+ version: 0.50.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: goodcop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.1.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: 11.2.0
61
+ version: 12.1.0
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: 11.2.0
68
+ version: 12.1.0
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: codecov
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -72,42 +86,42 @@ dependencies:
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 3.5.0
89
+ version: 3.6.0
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: 3.5.0
96
+ version: 3.6.0
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: fuubar
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: 2.0.0
103
+ version: 2.2.0
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: 2.0.0
110
+ version: 2.2.0
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: webmock
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: 2.1.0
117
+ version: 3.0.0
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
- version: 2.1.0
124
+ version: 3.0.0
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: vcr
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -128,6 +142,7 @@ executables: []
128
142
  extensions: []
129
143
  extra_rdoc_files: []
130
144
  files:
145
+ - ".circle-ruby"
131
146
  - ".gitignore"
132
147
  - ".prospectus"
133
148
  - ".rspec"
@@ -142,7 +157,9 @@ files:
142
157
  - dev/spec.yml
143
158
  - dev/version
144
159
  - lib/linodeapi.rb
160
+ - lib/linodeapi/errors.rb
145
161
  - lib/linodeapi/raw.rb
162
+ - lib/linodeapi/retryable.rb
146
163
  - linodeapi.gemspec
147
164
  - spec/fixtures/cassettes/basic_auth.yml
148
165
  - spec/fixtures/cassettes/basic_auth_fail.yml
@@ -173,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
173
190
  version: '0'
174
191
  requirements: []
175
192
  rubyforge_project:
176
- rubygems_version: 2.5.1
193
+ rubygems_version: 2.6.13
177
194
  signing_key:
178
195
  specification_version: 4
179
196
  summary: Linode API wrapper