ovh-provisioner 0.1.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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.gitlab-ci.yml +23 -0
  4. data/.rspec +2 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG +7 -0
  7. data/CONTRIBUTING.md +62 -0
  8. data/Gemfile +6 -0
  9. data/LICENSE +202 -0
  10. data/README.md +107 -0
  11. data/Rakefile +24 -0
  12. data/bin/console +32 -0
  13. data/bin/ovh_provisioner +27 -0
  14. data/bin/setup +23 -0
  15. data/lib/ovh/provisioner.rb +43 -0
  16. data/lib/ovh/provisioner/api_list.rb +158 -0
  17. data/lib/ovh/provisioner/api_object/api_object.rb +125 -0
  18. data/lib/ovh/provisioner/api_object/dedicated_server.rb +225 -0
  19. data/lib/ovh/provisioner/api_object/domain_zone.rb +115 -0
  20. data/lib/ovh/provisioner/api_object/ip.rb +83 -0
  21. data/lib/ovh/provisioner/api_object/record.rb +48 -0
  22. data/lib/ovh/provisioner/api_object/vrack.rb +92 -0
  23. data/lib/ovh/provisioner/cli.rb +173 -0
  24. data/lib/ovh/provisioner/cli_domain.rb +138 -0
  25. data/lib/ovh/provisioner/cli_ip.rb +64 -0
  26. data/lib/ovh/provisioner/cli_vrack.rb +71 -0
  27. data/lib/ovh/provisioner/init.rb +77 -0
  28. data/lib/ovh/provisioner/self_cli.rb +81 -0
  29. data/lib/ovh/provisioner/spawner.rb +63 -0
  30. data/lib/ovh/provisioner/version.rb +24 -0
  31. data/ovh-provisioner.gemspec +53 -0
  32. data/spec/config.yml +53 -0
  33. data/spec/helpers/highline_helper.rb +36 -0
  34. data/spec/ovh/provisioner/cli_domain_spec.rb +140 -0
  35. data/spec/ovh/provisioner/cli_ip_spec.rb +90 -0
  36. data/spec/ovh/provisioner/cli_spec.rb +186 -0
  37. data/spec/ovh/provisioner/cli_vrack_spec.rb +83 -0
  38. data/spec/ovh/provisioner/stubs/domain_stubs.rb +204 -0
  39. data/spec/ovh/provisioner/stubs/ip_stubs.rb +152 -0
  40. data/spec/ovh/provisioner/stubs/server_stubs.rb +146 -0
  41. data/spec/ovh/provisioner/stubs/vrack_stubs.rb +87 -0
  42. data/spec/ovh/provisioner_spec.rb +25 -0
  43. data/spec/spec_helper.rb +47 -0
  44. metadata +350 -0
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module OVH
20
+ module Provisioner
21
+ module APIObject
22
+ # Represent a Domain
23
+ class DomainZone < APIObject
24
+ attr_reader :nameservers, :dnssec_supported, :dnssec, :records
25
+
26
+ def to_s
27
+ "#{id}, dnssec #{dnssec_to_s}, #{records.list.size} records"
28
+ end
29
+
30
+ def details(record_lists = records)
31
+ "#{id} - dnssec: #{dnssec_to_s}\n"\
32
+ "#{(record_lists.format('type').lines[1..-1] || []).join('')}"
33
+ end
34
+
35
+ def add_record(subdomain, type, target, ttl = 0)
36
+ body = {
37
+ 'fieldType' => type.upcase,
38
+ 'target' => target,
39
+ 'ttl' => ttl.to_i
40
+ }
41
+ sub = subdomain
42
+ body['subDomain'] = sub unless sub.nil? || sub.empty?
43
+ answer = post('record', body)
44
+ post('refresh')
45
+ format(answer)
46
+ end
47
+
48
+ def rm_record(record)
49
+ out = delete("record/#{record.id}")
50
+ post('refresh')
51
+ " #{record}#{out.nil? ? '' : ": #{out}"}"
52
+ end
53
+
54
+ def rm_records(records)
55
+ outputs = records.parallel_map(:rm_record, [], Actor.current)
56
+ (["#{id}, remove records:"] + outputs.sort).join("\n")
57
+ end
58
+
59
+ def filter_records(config)
60
+ filter = generate_filter(
61
+ config['subdomain'] || '',
62
+ config['type'] || '',
63
+ config['target'] || '',
64
+ config['ttl'] || ''
65
+ )
66
+ targets = records.list.dup.keep_if do |record|
67
+ filter.call(record)
68
+ end.map(&:id)
69
+ Actor[Spawner::NAME].get('Record', *targets, parent: @url)
70
+ end
71
+
72
+ private
73
+
74
+ def set_general
75
+ general = get
76
+ @nameservers = general['nameServers']
77
+ @dnssec_supported = general['dnssecSupported']
78
+ end
79
+
80
+ def set_dnssec
81
+ @dnssec = get('dnssec')['status']
82
+ end
83
+
84
+ def set_records
85
+ @records = Actor[Spawner::NAME].get('Record', '', parent: @url)
86
+ end
87
+
88
+ def dnssec_to_s
89
+ dnssec_supported ? dnssec : 'not supported'
90
+ end
91
+
92
+ def generate_filter(subdomain, type, target, ttl)
93
+ lambda do |record|
94
+ record.subdomain.include?(subdomain) &&
95
+ (type.empty? || record.type == type.upcase) &&
96
+ record.target.include?(target) &&
97
+ (ttl == '' || record.ttl == ttl.to_i)
98
+ end
99
+ end
100
+
101
+ def format(answer)
102
+ return answer if answer.is_a? String
103
+
104
+ zone = answer['zone']
105
+ sub = answer['subDomain'] || ''
106
+ ttl = answer['ttl']
107
+ type = answer['fieldType']
108
+ target = answer['target']
109
+ "#{id}, add record:\n"\
110
+ " #{Record.print(zone, sub, ttl, type, target)}"
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module OVH
20
+ module Provisioner
21
+ module APIObject
22
+ # Represent an OVH IP
23
+ class IP < APIObject
24
+ attr_reader :organisation_id, :country
25
+ attr_reader :routed_to, :can_be_terminated, :type
26
+ attr_reader :description
27
+ attr_reader :reverse
28
+ attr_reader :kind
29
+
30
+ def add_reverse(reverse)
31
+ body = {
32
+ 'ipReverse' => id.split('/').first,
33
+ 'reverse' => reverse
34
+ }
35
+ response = post('reverse', body)
36
+ unless response['reverse'].nil?
37
+ response = "#{id} - reverse #{reverse} has been added"
38
+ @reverse = reverse['reverse']
39
+ end
40
+ response
41
+ end
42
+
43
+ def rm_reverse
44
+ response = delete("reverse/#{id.split('/').first}")
45
+ response ||= "#{id} - reverse #{reverse} has been removed"
46
+ response
47
+ end
48
+
49
+ def to_s
50
+ base = "#{id} - #{routed_to}(#{type}) - #{endable}"
51
+ %i[organisation_id country reverse description].each do |key|
52
+ value = send(key)
53
+ base += "\n #{key}: #{value}" unless value.nil?
54
+ end
55
+ base
56
+ end
57
+
58
+ private
59
+
60
+ def set_general
61
+ general = get
62
+ @organisation_id = general['organisationId']
63
+ @country = general['country']
64
+ routed = general['routedTo']
65
+ @routed_to = routed.nil? ? nil : routed['serviceName']
66
+ @can_be_terminated = general['canBeTerminated']
67
+ @type = general['type']
68
+ @description = general['description']
69
+ @kind = id.include?(':') ? 'ipv6' : 'ipv4'
70
+ end
71
+
72
+ def set_reverse
73
+ reverse = get("reverse/#{id.split('/').first}")
74
+ @reverse = reverse['reverse']
75
+ end
76
+
77
+ def endable
78
+ can_be_terminated ? 'endable' : 'non endable'
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module OVH
20
+ module Provisioner
21
+ module APIObject
22
+ # Represent a record in a domain zone
23
+ class Record < APIObject
24
+ attr_reader :target, :ttl, :zone, :type, :subdomain
25
+
26
+ def self.print(zone, subdomain, ttl, type, target)
27
+ "#{subdomain.empty? ? "#{zone}." : subdomain} "\
28
+ "#{ttl} #{type} #{target}".strip
29
+ end
30
+
31
+ def to_s
32
+ self.class.print(zone, subdomain, ttl, type, target)
33
+ end
34
+
35
+ private
36
+
37
+ def set_general
38
+ general = get
39
+ @target = general['target']
40
+ @ttl = general['ttl']
41
+ @zone = general['zone']
42
+ @type = general['fieldType']
43
+ @subdomain = general['subDomain']
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module OVH
20
+ module Provisioner
21
+ module APIObject
22
+ # Represent a VRack
23
+ class Vrack < APIObject
24
+ attr_reader :name, :description, :dedicated_servers, :tasks
25
+
26
+ def add(api_object)
27
+ path = path_from(api_object)
28
+ ok, msg = parse_task(post(path, path => api_object.id))
29
+ "#{api_object.id}: #{ok ? 'ok' : 'failed'}\n #{msg}"
30
+ end
31
+
32
+ def remove(api_object)
33
+ path = "#{path_from(api_object)}/#{api_object.id}"
34
+ ok, msg = parse_task(delete(path))
35
+ "#{api_object.id}: #{ok ? 'ok' : 'failed'}\n #{msg}"
36
+ end
37
+
38
+ def to_s
39
+ "#{name}: #{id}#{" - #{description}" unless description.empty?}" \
40
+ "#{list_to_s(:dedicated_servers)}" \
41
+ "#{list_to_s(:tasks)}"
42
+ end
43
+
44
+ private
45
+
46
+ def set_general
47
+ general = get
48
+ @name = general['name']
49
+ @description = general['description']
50
+ end
51
+
52
+ def set_dedicated_server
53
+ @dedicated_servers = get('dedicatedServer')
54
+ end
55
+
56
+ def set_tasks
57
+ tasks = get('task')
58
+ futures = tasks.map { |t| future.get("task/#{t}") }
59
+ @tasks = futures.map do |future|
60
+ _ok, msg = parse_task(future.value)
61
+ msg
62
+ end
63
+ end
64
+
65
+ def list_to_s(list_sym)
66
+ list = send(list_sym)
67
+ return '' if list.nil? || list.empty?
68
+
69
+ header = ["\n #{list_sym}:"]
70
+ tail = list.map { |i| " #{i}" }.sort
71
+ (header + tail).join("\n")
72
+ end
73
+
74
+ def path_from(api_object)
75
+ name = api_object.class.classname
76
+ "#{name[0..0].downcase}#{name[1..-1]}"
77
+ end
78
+
79
+ def parse_task(msg)
80
+ return [false, msg] if msg.is_a?(String)
81
+
82
+ method = /^[[:lower:]]+/.match(msg['function']).to_a.first
83
+ [
84
+ true,
85
+ "#{msg['status']} #{msg['serviceName']}.#{method}" \
86
+ "(#{msg['targetDomain']})"
87
+ ]
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'thor'
20
+ require 'pp'
21
+ require 'ruby-progressbar'
22
+ require 'highline/import'
23
+ Dir[File.dirname(__FILE__) + '/cli*.rb'].each { |file| require file }
24
+
25
+ # Monkey patch Thor to fix #261
26
+ module ThorPatching
27
+ def banner(command, namespace = nil, subcommand = false)
28
+ array = super.split(' ')
29
+ array[1] = array[1].tr('_', '')[3..-1] if array[1].start_with?('cli_')
30
+ array.join(' ')
31
+ end
32
+ end
33
+
34
+ # Prepending the patch
35
+ class Thor
36
+ class << self
37
+ prepend ThorPatching
38
+ end
39
+ end
40
+
41
+ module OVH
42
+ module Provisioner
43
+ # The command line runner
44
+ class Cli < Thor # rubocop:disable Metrics/ClassLength
45
+ desc 'version', 'print OVH Provisioner\'s version information'
46
+ def version
47
+ say "OVH Provisioner version #{OVH::Provisioner::VERSION}"
48
+ end
49
+ map %w[-v --version] => :version
50
+
51
+ desc 'list targets', 'print the list of your dedicated servers'
52
+ def list(*targets)
53
+ spawner = Provisioner.init(options)
54
+ puts spawner
55
+ .get('DedicatedServer', *self.class.all(targets))
56
+ .format('vrack', 'flavor')
57
+ end
58
+
59
+ option(
60
+ :force,
61
+ type: :boolean,
62
+ desc: 'Force the installation even if the server is already installed',
63
+ default: false,
64
+ aliases: ['-f']
65
+ )
66
+ option(
67
+ :template,
68
+ desc: 'Template to install',
69
+ aliases: ['-t']
70
+ )
71
+ option(
72
+ :ssh_key_name,
73
+ desc: 'Name of the ssh key to install',
74
+ aliases: ['-n']
75
+ )
76
+ option(
77
+ :use_distrib_kernel,
78
+ type: :boolean,
79
+ desc: 'Use official kernel instead of OVH\'s (default = false)',
80
+ aliases: ['-d']
81
+ )
82
+ option(
83
+ :custom_hostname,
84
+ desc: 'Hostname of the server (default = reverse)',
85
+ aliases: ['-h']
86
+ )
87
+ desc 'install targets', 'install/reinstall one or more dedicated servers'
88
+ def install(*targets)
89
+ spawner = Provisioner.init(options)
90
+ servers = spawner.get('DedicatedServer', *self.class.all(targets))
91
+ self.class.ask_validation(
92
+ 'You are going to (re)install those servers:',
93
+ servers.format('install_status', 'flavor')
94
+ )
95
+ servers.puts_each(:install)
96
+ end
97
+
98
+ NAME_SCHEME = [
99
+ :name_scheme,
100
+ desc: 'Name scheme to use for the servers, ex: %<flavor_tag>s-%<id>s',
101
+ aliases: ['-n']
102
+ ].freeze
103
+ NAME_DOMAIN = [
104
+ :name_domain,
105
+ desc: 'Domain of the servers, ex: test.com',
106
+ aliases: ['-d']
107
+ ].freeze
108
+
109
+ option(*NAME_SCHEME)
110
+ option(*NAME_DOMAIN)
111
+ desc 'rename targets', 'rename one or more dedicated servers'
112
+ def rename(*targets)
113
+ servers, domain = init_rename(targets)
114
+ # Add check on duplication?
115
+ self.class.ask_validation(
116
+ 'You are going to rename those servers:',
117
+ servers.list.map do |s|
118
+ " #{s.reverse} => #{s.newname}.#{domain.id}"
119
+ end.join("\n")
120
+ )
121
+ servers.puts_each(:rename, [domain])
122
+ puts 'To complete renaming, call "set_reverse" in a few minute'
123
+ end
124
+
125
+ option(*NAME_SCHEME)
126
+ option(*NAME_DOMAIN)
127
+ desc 'set_reverse targets', 'set the reverse for a server'
128
+ def set_reverse(*targets) # rubocop:disable Style/AccessorMethodName
129
+ servers, domain = init_rename(targets)
130
+ servers.puts_each(:define_reverse, [domain])
131
+ end
132
+
133
+ # TODO: test it!
134
+ desc 'ipmi target', 'create ipmi interface and launch javaws on it'
135
+ def ipmi(*targets)
136
+ spawner = Provisioner.init(options)
137
+ servers = spawner.get('DedicatedServer', *self.class.all(targets))
138
+ list = servers.list
139
+ unless list.size == 1
140
+ puts 'Please select one and only one target! You have targeted:'
141
+ puts servers.format('flavor')
142
+ exit 1
143
+ end
144
+ list.first.ipmi
145
+ end
146
+
147
+ desc 'get url', 'execute a get on url'
148
+ def get(url)
149
+ Provisioner.init(options)
150
+ puts Provisioner.client.get url
151
+ end
152
+
153
+ desc 'vrack SUBCOMMAND ...ARGS', 'Manage Vracks'
154
+ subcommand 'vrack', CliVrack
155
+
156
+ desc 'domain SUBCOMMAND ...ARGS', 'Manage Domains'
157
+ subcommand 'domain', CliDomain
158
+
159
+ desc 'ip SUBCOMMAND ...ARGS', 'Manage IPs'
160
+ subcommand 'ip', CliIP
161
+
162
+ no_commands do
163
+ def init_rename(targets)
164
+ spawner = Provisioner.init(options)
165
+ servers = spawner.get('DedicatedServer', *self.class.all(targets))
166
+ name_domain = Provisioner.config['name_domain']
167
+ domain = spawner.get('DomainZone', id: name_domain)
168
+ [servers, domain]
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end