ovh-provisioner 0.1.0

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