pvcglue 0.9.3 → 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/pvc +4 -0
- data/lib/pvcglue.rb +55 -15
- data/lib/pvcglue/cli.rb +14 -2
- data/lib/pvcglue/cloud.rb +42 -26
- data/lib/pvcglue/cloud_providers.rb +34 -0
- data/lib/pvcglue/cloud_providers/digital_ocean.rb +79 -0
- data/lib/pvcglue/cloud_providers/linode.rb +129 -0
- data/lib/pvcglue/configuration.rb +5 -0
- data/lib/pvcglue/connection.rb +2 -0
- data/lib/pvcglue/docs.rb +97 -0
- data/lib/pvcglue/manager.rb +0 -1
- data/lib/pvcglue/minion.rb +30 -22
- data/lib/pvcglue/packages.rb +4 -0
- data/lib/pvcglue/packages/apt_repos.rb +33 -4
- data/lib/pvcglue/packages/authorized_keys.rb +24 -8
- data/lib/pvcglue/packages/firewall.rb +1 -0
- data/lib/pvcglue/packages/load_balancer.rb +4 -1
- data/lib/pvcglue/packages/manager.rb +24 -3
- data/lib/pvcglue/packages/ssh_key_check.rb +7 -1
- data/lib/pvcglue/packages/ssl.rb +13 -3
- data/lib/pvcglue/pvcify.rb +1 -1
- data/lib/pvcglue/stack.rb +38 -42
- data/lib/pvcglue/templates/pvc_manager.toml.erb +182 -0
- data/lib/pvcglue/version.rb +1 -1
- data/pvcglue.gemspec +4 -2
- metadata +22 -25
- data/lib/pvcglue/all_the_things.rb +0 -7
- data/lib/pvcglue/digital_ocean.rb +0 -21
@@ -60,6 +60,7 @@ module Pvcglue
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def configure_manager
|
63
|
+
byebug
|
63
64
|
manager = ask('What is the IP address or host name of the manager?')
|
64
65
|
default = !no?('Will this be the default manager? (Y/n)')
|
65
66
|
file_name = default ? user_file_name : project_file_name
|
@@ -145,6 +146,10 @@ module Pvcglue
|
|
145
146
|
File.join(tmp_dir, 'pvcglue')
|
146
147
|
end
|
147
148
|
|
149
|
+
def template_override_dir
|
150
|
+
File.join(application_dir, 'config', 'pvcglue', 'templates')
|
151
|
+
end
|
152
|
+
|
148
153
|
def cloud_cache_file_name
|
149
154
|
# Just in case the Rails project hasn't yet been run, make sure the tmp
|
150
155
|
# dir exists.
|
data/lib/pvcglue/connection.rb
CHANGED
@@ -75,6 +75,7 @@ module Pvcglue
|
|
75
75
|
|
76
76
|
def system_command?(cmd)
|
77
77
|
Pvcglue.logger.debug { cmd }
|
78
|
+
Pvcglue.docs.log_cmd(cmd)
|
78
79
|
system(cmd)
|
79
80
|
Pvcglue.logger.debug { "exit_code=#{$?.exitstatus}" }
|
80
81
|
$?
|
@@ -170,6 +171,7 @@ module Pvcglue
|
|
170
171
|
end
|
171
172
|
|
172
173
|
def write_to_file(user, data, file, owner = nil, group = nil, permissions = nil)
|
174
|
+
Pvcglue.docs.log_file_write(user: user, style: :shell, data: data, file: file, owner: owner, group: group, permissions: permissions)
|
173
175
|
tmp_file = Tempfile.new('pvc')
|
174
176
|
begin
|
175
177
|
tmp_file.write(data)
|
data/lib/pvcglue/docs.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
module Pvcglue
|
2
|
+
class Docs
|
3
|
+
ROLLS = {
|
4
|
+
lb: 'Load Balancer'
|
5
|
+
}
|
6
|
+
|
7
|
+
def initialize(enabled)
|
8
|
+
@enabled = enabled
|
9
|
+
@data = []
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :enabled, :data, :current_minion, :collecting
|
13
|
+
|
14
|
+
def add(s)
|
15
|
+
return unless collecting
|
16
|
+
data << s
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_paragraph(text)
|
20
|
+
add('')
|
21
|
+
add(text)
|
22
|
+
add('')
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_annotation(text)
|
26
|
+
add('')
|
27
|
+
add("> #{text}")
|
28
|
+
add('')
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def level_1_roles(minion)
|
33
|
+
return unless enabled
|
34
|
+
self.current_minion = minion
|
35
|
+
if minion
|
36
|
+
self.collecting = true
|
37
|
+
level_1(minion.roles.map { |role| ROLLS[role.to_sym] || role }.join(', '))
|
38
|
+
else
|
39
|
+
self.collecting = false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def level_1(heading)
|
44
|
+
return unless enabled
|
45
|
+
add_header(1, heading)
|
46
|
+
end
|
47
|
+
|
48
|
+
def level_2(heading)
|
49
|
+
return unless enabled
|
50
|
+
add_header(2, heading)
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_header(level, heading)
|
54
|
+
return unless enabled
|
55
|
+
add('')
|
56
|
+
add("#{'#'*level} #{heading}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_item(options)
|
60
|
+
return unless enabled
|
61
|
+
options = ::SafeMash.new(options)
|
62
|
+
add_header(3, options.heading)
|
63
|
+
add('----')
|
64
|
+
yield
|
65
|
+
add_paragraph(options.body) if options.body && options.body.present?
|
66
|
+
if options.reference && options.reference.present?
|
67
|
+
add('')
|
68
|
+
add("See also: [#{options.reference}](#{options.reference})")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def log_file_write(options)
|
73
|
+
return unless enabled
|
74
|
+
options = ::SafeMash.new(options)
|
75
|
+
add_annotation("Write data to `#{options.file}`")
|
76
|
+
add_block(options)
|
77
|
+
end
|
78
|
+
|
79
|
+
def log_cmd(cmd)
|
80
|
+
return unless enabled
|
81
|
+
# add_block(data: cmd, style: 'shell')
|
82
|
+
add("> `> #{cmd}`<br />")
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_block(options)
|
86
|
+
options = ::SafeMash.new(options)
|
87
|
+
add("```#{options.style}")
|
88
|
+
add(options.data)
|
89
|
+
add('```')
|
90
|
+
end
|
91
|
+
|
92
|
+
def done
|
93
|
+
return unless enabled
|
94
|
+
File.write('/home/andrew/projects/slate-pvc/source/includes/_logs.md', data.join("\n"))
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/pvcglue/manager.rb
CHANGED
data/lib/pvcglue/minion.rb
CHANGED
@@ -3,10 +3,11 @@ module Pvcglue
|
|
3
3
|
include Hashie::Extensions::Mash::SafeAssignment
|
4
4
|
|
5
5
|
def build!
|
6
|
-
#
|
6
|
+
minion = self # for readability
|
7
|
+
|
7
8
|
Pvcglue.logger.info('BUILD') { "Building #{machine_name}" }
|
9
|
+
Pvcglue.docs.level_1_roles(minion)
|
8
10
|
|
9
|
-
minion = self # for readability
|
10
11
|
Pvcglue::Packages::SshKeyCheck.apply(minion)
|
11
12
|
Pvcglue::Packages::AptRepos.apply(minion)
|
12
13
|
Pvcglue::Packages::AptUpdate.apply(minion)
|
@@ -37,40 +38,47 @@ module Pvcglue
|
|
37
38
|
public_ip.present?
|
38
39
|
end
|
39
40
|
|
41
|
+
def pvc_cloud_provider
|
42
|
+
byebug unless machine_options.cloud_provider || default_cloud_provider
|
43
|
+
@pvc_cloud_provider ||= Pvcglue::CloudProviders.init(machine_options.cloud_provider || default_cloud_provider)
|
44
|
+
end
|
45
|
+
|
40
46
|
def create!
|
41
|
-
|
42
|
-
Pvcglue.logger.warn("Provisioning a machine for #{machine_name} on #{cloud_provider.name}...")
|
47
|
+
Pvcglue.logger.warn("Provisioning a machine for #{machine_name} on #{pvc_cloud_provider.name}...")
|
43
48
|
|
44
49
|
# TODO: Tags. production, staging, load-balancer, web, worker, database, postgress, cache, memcache...
|
50
|
+
|
45
51
|
name = machine_name
|
46
|
-
|
47
|
-
image =
|
48
|
-
region =
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
52
|
+
capacity = pvc_cloud_provider.options.capacity
|
53
|
+
image = pvc_cloud_provider.options.image
|
54
|
+
region = pvc_cloud_provider.options.region
|
55
|
+
if pvc_cloud_provider.options.initial_users
|
56
|
+
ssh_keys = pvc_cloud_provider.options.initial_users.map { |description, ssh_key| ssh_key }
|
57
|
+
else
|
58
|
+
ssh_keys = []
|
59
|
+
end
|
60
|
+
# backups = cloud_provider.backups.nil? ? true : cloud_provider.backups # default to true -- safety first!
|
61
|
+
backups = pvc_cloud_provider.options.backups.nil? ? true : machine_options.cloud_provider.backups # default to true -- safety first!
|
62
|
+
tags = pvc_cloud_provider.options.tags
|
63
|
+
group = pvc_cloud_provider.options.group
|
64
|
+
|
65
|
+
options = ::SafeMash.new(
|
59
66
|
name: name,
|
60
67
|
region: region,
|
61
68
|
image: image,
|
62
|
-
|
69
|
+
capacity: capacity,
|
63
70
|
ssh_keys: ssh_keys,
|
64
71
|
backups: backups,
|
65
72
|
ipv6: false,
|
66
73
|
private_networking: true,
|
67
74
|
user_data: '',
|
68
75
|
monitoring: true,
|
69
|
-
tags: tags
|
76
|
+
tags: tags,
|
77
|
+
group: group,
|
70
78
|
)
|
71
|
-
|
72
|
-
|
73
|
-
|
79
|
+
|
80
|
+
machine = pvc_cloud_provider.create(options)
|
81
|
+
self.machine = machine
|
74
82
|
true
|
75
83
|
end
|
76
84
|
|
data/lib/pvcglue/packages.rb
CHANGED
@@ -21,13 +21,41 @@ module Pvcglue
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def install!
|
24
|
+
docs.level_2('Repositories')
|
25
|
+
|
26
|
+
# TODO: Make this a package that checks for the existence of software-properties-common
|
27
|
+
#echo 'Acquire::ForceIPv4 "true";' | tee /etc/apt/apt.conf.d/99force-ipv4
|
28
|
+
docs.set_item(
|
29
|
+
heading: 'Force use of IPv4',
|
30
|
+
body: 'Needed for Linode as there were intermittent problems connecting to their repositories over IPv6 (February 2017)'
|
31
|
+
) do
|
32
|
+
connection.write_to_file(:root, 'Acquire::ForceIPv4 "true";', '/etc/apt/apt.conf.d/99force-ipv4')
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
docs.set_item(
|
37
|
+
heading: 'Update Repositories'
|
38
|
+
) do
|
39
|
+
connection.run!(:root, '', 'apt update -y')
|
40
|
+
end
|
41
|
+
docs.set_item(
|
42
|
+
heading: 'Install Requirements',
|
43
|
+
body: 'Install the requirements for adding more repositories'
|
44
|
+
) do
|
45
|
+
connection.run!(:root, '', 'apt install -y software-properties-common python-software-properties')
|
46
|
+
end
|
24
47
|
# These could be refactored into packages. :)
|
25
48
|
|
26
49
|
if nginx_needed?
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
50
|
+
docs.set_item(
|
51
|
+
heading: 'Nginx',
|
52
|
+
body: 'Install the latest version using the Phusion repos.',
|
53
|
+
reference: 'https://www.phusionpassenger.com/library/install/nginx/install/oss/xenial/'
|
54
|
+
) do
|
55
|
+
connection.run!(:root, '', 'apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7')
|
56
|
+
connection.run!(:root, '', 'apt-get install -y apt-transport-https ca-certificates')
|
57
|
+
connection.write_to_file(:root, PASSENGER_SOURCES_LIST_DATA, PASSENGER_SOURCES_LIST_FILENAME)
|
58
|
+
end
|
31
59
|
end
|
32
60
|
|
33
61
|
if node_js_needed?
|
@@ -40,6 +68,7 @@ module Pvcglue
|
|
40
68
|
connection.run!(:root, '', 'add-apt-repository "deb http://apt.postgresql.org/pub/repos/apt/ xenial-pgdg main"')
|
41
69
|
connection.run!(:root, '', 'wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -')
|
42
70
|
end
|
71
|
+
# byebug # docs resume here
|
43
72
|
|
44
73
|
set_minion_state(:apt_repos_updated_at, Time.now.utc)
|
45
74
|
end
|
@@ -11,22 +11,38 @@ module Pvcglue
|
|
11
11
|
|
12
12
|
def install!
|
13
13
|
# TODO: Safety check to see if user is locking himself out. :)
|
14
|
-
|
15
|
-
if data.count == 0
|
16
|
-
raise('No authorized keys found for root users!')
|
14
|
+
if manager_first_bootstrap?
|
17
15
|
# TODO: work out system for pvc-manager access
|
18
16
|
data = [`cat ~/.ssh/id_rsa.pub`.strip]
|
17
|
+
else
|
18
|
+
data = minion.get_root_authorized_keys_data
|
19
|
+
if data.count == 0
|
20
|
+
raise('No authorized keys found for root users!')
|
21
|
+
end
|
19
22
|
end
|
20
23
|
connection.write_to_file(:root, data.join("\n"), '/root/.ssh/authorized_keys')
|
21
24
|
|
22
25
|
connection.mkdir_p(:root, "/home/#{user_name}/.ssh", user_name, user_name, '0700')
|
23
|
-
|
24
|
-
if
|
25
|
-
raise('No authorized keys found for users!')
|
26
|
-
# TODO: work out system for pvc-manager access
|
26
|
+
|
27
|
+
if manager_first_bootstrap?
|
27
28
|
data = [`cat ~/.ssh/id_rsa.pub`.strip]
|
29
|
+
else
|
30
|
+
data = minion.get_users_authorized_keys_data
|
31
|
+
if data.count == 0
|
32
|
+
raise('No authorized keys found for users!')
|
33
|
+
# TODO: work out system for pvc-manager access
|
34
|
+
end
|
28
35
|
end
|
29
|
-
connection.write_to_file(:root, data.join("\n"),
|
36
|
+
connection.write_to_file(:root, data.join("\n"), user_authorized_keys_file_name, user_name, user_name, '0644')
|
37
|
+
end
|
38
|
+
|
39
|
+
def user_authorized_keys_file_name
|
40
|
+
"/home/#{user_name}/.ssh/authorized_keys"
|
41
|
+
end
|
42
|
+
|
43
|
+
def manager_first_bootstrap?
|
44
|
+
return false unless has_role?(:manager)
|
45
|
+
@manager_first_bootstrap ||= !Pvcglue::Packages::Manager.configuration_exists?
|
30
46
|
end
|
31
47
|
end
|
32
48
|
end
|
@@ -21,6 +21,7 @@ module Pvcglue
|
|
21
21
|
unless has_role?(:manager)
|
22
22
|
minion.cloud.minions.each do |other_minion_name, other_minion|
|
23
23
|
next if other_minion_name == minion.machine_name
|
24
|
+
next unless other_minion.provisioned?
|
24
25
|
connection.run!(:root, '', "ufw allow from #{other_minion.private_ip}")
|
25
26
|
end
|
26
27
|
end
|
@@ -6,6 +6,8 @@ module Pvcglue
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def install!
|
9
|
+
sync_maintenance_files
|
10
|
+
|
9
11
|
if Pvcglue.cloud.ssl_mode == :acme && !connection.file_exists?(:root, Pvcglue.cloud.nginx_ssl_crt_file_name)
|
10
12
|
# Don't include the SSL stuff in the Nginx config until we have a cert,
|
11
13
|
# but Nginx has to be configured to get the cert from Let's Encrypt
|
@@ -24,7 +26,6 @@ module Pvcglue
|
|
24
26
|
nginx_restart!
|
25
27
|
end
|
26
28
|
|
27
|
-
sync_maintenance_files
|
28
29
|
end
|
29
30
|
|
30
31
|
def nginx_restart!
|
@@ -56,10 +57,12 @@ module Pvcglue
|
|
56
57
|
end
|
57
58
|
|
58
59
|
def sync_maintenance_files
|
60
|
+
Pvcglue.logger.debug { 'Synchronizing maintenance mode files' }
|
59
61
|
source_dir = Pvcglue.configuration.app_maintenance_files_dir
|
60
62
|
dest_dir = Pvcglue.cloud.maintenance_files_dir
|
61
63
|
maintenance_file_name = File.join(source_dir, 'maintenance.html')
|
62
64
|
unless File.exists?(maintenance_file_name)
|
65
|
+
Pvcglue.logger.debug { 'Creating default maintenance mode files' }
|
63
66
|
# TODO: Make this use a template
|
64
67
|
`mkdir -p #{source_dir}`
|
65
68
|
File.write(maintenance_file_name, '-Maintenance Mode-')
|
@@ -48,6 +48,14 @@ module Pvcglue
|
|
48
48
|
new.get_configuration
|
49
49
|
end
|
50
50
|
|
51
|
+
def self.configuration_exists?
|
52
|
+
new.configuration_exists?
|
53
|
+
end
|
54
|
+
|
55
|
+
def configuration_exists?
|
56
|
+
connection.file_exists?(user_name, ::Pvcglue::Manager.manager_file_name)
|
57
|
+
end
|
58
|
+
|
51
59
|
def get_configuration
|
52
60
|
# if connection.file_exists?(user_name, ::Pvcglue::Manager.manager_file_name)
|
53
61
|
# data = connection.read_from_file(user_name, ::Pvcglue::Manager.manager_file_name)
|
@@ -56,8 +64,12 @@ module Pvcglue
|
|
56
64
|
# raise("Remote manager file not found: #{::Pvcglue::Manager.manager_file_name}")
|
57
65
|
# end
|
58
66
|
data = '' # to use `data` in block
|
59
|
-
Pvcglue.
|
60
|
-
data =
|
67
|
+
if Pvcglue.command_line_options[:cloud_manager_override]
|
68
|
+
data = File.read(Pvcglue.command_line_options[:cloud_manager_override])
|
69
|
+
else
|
70
|
+
Pvcglue.filter_verbose do
|
71
|
+
data = connection.read_from_file(user_name, ::Pvcglue::Manager.manager_file_name)
|
72
|
+
end
|
61
73
|
end
|
62
74
|
::Pvcglue.cloud.data = TOML.parse(data)
|
63
75
|
end
|
@@ -67,6 +79,13 @@ module Pvcglue
|
|
67
79
|
end
|
68
80
|
|
69
81
|
def push_configuration
|
82
|
+
raise('Not supported for local manager') if Pvcglue.command_line_options[:cloud_manager_override]
|
83
|
+
|
84
|
+
test_data = File.read(::Pvcglue.cloud.local_file_name)
|
85
|
+
TOML.parse(test_data) # At least make sure it's valid TOML
|
86
|
+
|
87
|
+
# TODO: More in-depth validations
|
88
|
+
|
70
89
|
connection.upload_file(user_name, ::Pvcglue.cloud.local_file_name, ::Pvcglue::Manager.manager_file_name, nil, nil, '600')
|
71
90
|
git_commit!
|
72
91
|
raise('Error saving configuration') unless working_directory_clean?
|
@@ -92,8 +111,10 @@ module Pvcglue
|
|
92
111
|
if connection.file_exists?(user_name, ::Pvcglue::Manager.manager_file_name)
|
93
112
|
data = connection.read_from_file(user_name, ::Pvcglue::Manager.manager_file_name)
|
94
113
|
else
|
95
|
-
|
114
|
+
template = Tilt.new(Pvcglue.template_file_name('pvc_manager.toml.erb'))
|
115
|
+
data = template.render(self, minion: minion)
|
96
116
|
end
|
117
|
+
|
97
118
|
file_name = ::Pvcglue.cloud.local_file_name
|
98
119
|
if File.exist?(file_name)
|
99
120
|
backup_file_name = ::Pvcglue.configuration.versioned_filename(file_name)
|
@@ -2,8 +2,14 @@ module Pvcglue
|
|
2
2
|
class Packages
|
3
3
|
class SshKeyCheck < Pvcglue::Packages
|
4
4
|
def installed?
|
5
|
+
docs.level_2('Status')
|
5
6
|
# This has the side effect of adding the server to known_hosts file, to prevent needing interactive prompt
|
6
|
-
|
7
|
+
docs.set_item(
|
8
|
+
heading: 'Verify Connection',
|
9
|
+
body: 'Connect to the machine and wait until it''s ready (with automatic retry). Also add the machine to the `known_hosts` file, to prevent an interactive prompt'
|
10
|
+
) do
|
11
|
+
connection.ssh_retry_wait(:root, '-o strictHostKeyChecking=no', 'echo', 30, 1)
|
12
|
+
end
|
7
13
|
true
|
8
14
|
end
|
9
15
|
end
|
data/lib/pvcglue/packages/ssl.rb
CHANGED
@@ -21,9 +21,19 @@ module Pvcglue
|
|
21
21
|
force_option = Pvcglue.command_line_options[:force_cert] ? '--force ' : ''
|
22
22
|
debug_option = Pvcglue.logger.level == 0 ? '--debug ' : ''
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
begin
|
25
|
+
|
26
|
+
# Test with http://www.example.com/.well-known/acme-challenge/test.html
|
27
|
+
base_name = "test-#{SecureRandom.hex}.html"
|
28
|
+
verification_file_name = File.join(Pvcglue.cloud.letsencrypt_full, base_name)
|
29
|
+
connection.write_to_file(:root, "Everything's shiny, Cap'n. Not to fret.", verification_file_name, 'root', 'www-data', '660')
|
30
|
+
|
31
|
+
unless Net::HTTP.get(first_domain, "/.well-known/acme-challenge/#{base_name}") =~ /shiny/
|
32
|
+
Pvcglue.logger.error("Unable to connect to #{first_domain} at #{minion.public_ip}")
|
33
|
+
raise(Thor::Error, 'Please fix and then restart.')
|
34
|
+
end
|
35
|
+
ensure
|
36
|
+
# TODO: Delete verification file
|
27
37
|
end
|
28
38
|
|
29
39
|
result = connection.ssh?(:root, '', "/root/.acme.sh/acme.sh #{debug_option}#{staging_option}#{force_option}--issue #{domain_options} -w #{Pvcglue.cloud.letsencrypt_root}")
|