subspace 2.5.7 → 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +77 -21
  5. data/UPGRADING.md +10 -0
  6. data/ansible/roles/common/defaults/main.yml +0 -1
  7. data/ansible/roles/common/files/sudoers-service +1 -1
  8. data/ansible/roles/common/tasks/main.yml +18 -7
  9. data/ansible/roles/common/tasks/no_swap.yml +26 -0
  10. data/ansible/roles/common/templates/motd +1 -1
  11. data/ansible/roles/common/templates/motd2 +1 -1
  12. data/ansible/roles/delayed_job/tasks/main.yml +1 -1
  13. data/ansible/roles/memcache/defaults/main.yml +2 -0
  14. data/ansible/roles/memcache/tasks/main.yml +16 -1
  15. data/ansible/roles/newrelic-infra/tasks/main.yml +3 -3
  16. data/ansible/roles/nginx/tasks/main.yml +12 -3
  17. data/ansible/roles/puma/tasks/main.yml +32 -20
  18. data/ansible/roles/puma/templates/puma-systemd.service +36 -0
  19. data/ansible/roles/puma/templates/puma-systemd.socket +14 -0
  20. data/ansible/roles/puma/templates/puma.rb +5 -3
  21. data/ansible/roles/rails/defaults/main.yml +0 -7
  22. data/ansible/roles/redis/tasks/main.yml +7 -0
  23. data/ansible/roles/resque/tasks/main.yml +14 -0
  24. data/ansible/roles/resque/templates/resque-monit-rc +4 -0
  25. data/ansible/roles/resque/templates/resque-systemd.service +54 -0
  26. data/ansible/roles/ruby-common/tasks/main.yml +1 -16
  27. data/ansible/roles/sidekiq/defaults/main.yml +1 -1
  28. data/ansible/roles/sidekiq/tasks/main.yml +11 -15
  29. data/ansible/roles/sidekiq/templates/sidekiq-monit-rc +1 -1
  30. data/ansible/roles/sidekiq/templates/sidekiq-systemd.service +62 -0
  31. data/ansible/roles/tailscale/defaults/main.yml +2 -0
  32. data/ansible/roles/tailscale/tasks/main.yml +22 -0
  33. data/exe/subspace +1 -2
  34. data/lib/subspace/cli.rb +50 -14
  35. data/lib/subspace/commands/ansible.rb +16 -1
  36. data/lib/subspace/commands/base.rb +20 -5
  37. data/lib/subspace/commands/bootstrap.rb +16 -21
  38. data/lib/subspace/commands/configure.rb +2 -2
  39. data/lib/subspace/commands/exec.rb +20 -0
  40. data/lib/subspace/commands/init.rb +94 -45
  41. data/lib/subspace/commands/inventory.rb +45 -0
  42. data/lib/subspace/commands/maintain.rb +1 -1
  43. data/lib/subspace/commands/provision.rb +1 -3
  44. data/lib/subspace/commands/{vars.rb → secrets.rb} +6 -5
  45. data/lib/subspace/commands/ssh.rb +10 -8
  46. data/lib/subspace/commands/terraform.rb +83 -0
  47. data/lib/subspace/inventory.rb +144 -0
  48. data/lib/subspace/version.rb +1 -1
  49. data/subspace.gemspec +8 -1
  50. data/template/{provision → subspace}/.gitignore +3 -0
  51. data/template/subspace/ansible.cfg.erb +16 -0
  52. data/template/subspace/group_vars/all.erb +28 -0
  53. data/template/subspace/group_vars/template.erb +26 -0
  54. data/template/{provision → subspace}/hosts.erb +0 -0
  55. data/template/subspace/inventory.yml.erb +11 -0
  56. data/template/{provision → subspace}/playbook.yml.erb +2 -5
  57. data/template/{provision/vars → subspace/secrets}/template.erb +0 -0
  58. data/template/{provision → subspace}/templates/application.yml.template +0 -0
  59. data/template/subspace/templates/authorized_keys.erb +1 -0
  60. data/template/subspace/terraform/.gitignore +2 -0
  61. data/template/subspace/terraform/template/main-oxenwagen.tf.erb +116 -0
  62. data/template/subspace/terraform/template/main-workhorse.tf.erb +41 -0
  63. data/template/subspace/terraformrc.erb +9 -0
  64. data/terraform/modules/s3_backend/README +2 -0
  65. data/terraform/modules/s3_backend/dynamodb.tf +1 -0
  66. data/terraform/modules/s3_backend/iam_user.tf +38 -0
  67. data/terraform/modules/s3_backend/main.tf +39 -0
  68. data/terraform/modules/s3_backend/state_bucket.tf +14 -0
  69. metadata +45 -39
  70. data/ansible/roles/monit/files/monit-http.conf +0 -3
  71. data/ansible/roles/monit/files/sudoers-monit +0 -1
  72. data/ansible/roles/monit/handlers/main.yml +0 -14
  73. data/ansible/roles/monit/tasks/main.yml +0 -34
  74. data/ansible/roles/mtpereira.passenger/.bumpversion.cfg +0 -7
  75. data/ansible/roles/mtpereira.passenger/.gitignore +0 -2
  76. data/ansible/roles/mtpereira.passenger/LICENSE +0 -20
  77. data/ansible/roles/mtpereira.passenger/README.md +0 -31
  78. data/ansible/roles/mtpereira.passenger/defaults/main.yml +0 -5
  79. data/ansible/roles/mtpereira.passenger/handlers/main.yml +0 -8
  80. data/ansible/roles/mtpereira.passenger/meta/.galaxy_install_info +0 -1
  81. data/ansible/roles/mtpereira.passenger/meta/main.yml +0 -21
  82. data/ansible/roles/mtpereira.passenger/tasks/apt.yml +0 -13
  83. data/ansible/roles/mtpereira.passenger/tasks/main.yml +0 -8
  84. data/ansible/roles/mtpereira.passenger/tasks/pkg.yml +0 -35
  85. data/ansible/roles/mtpereira.passenger/tasks/service.yml +0 -8
  86. data/ansible/roles/passenger/files/sudoers-passenger +0 -1
  87. data/ansible/roles/passenger/meta/main.yml +0 -6
  88. data/ansible/roles/passenger/tasks/main.yml +0 -5
  89. data/ansible/roles/postgis/defaults/main.yml +0 -2
  90. data/ansible/roles/puma/defaults/main.yml +0 -5
  91. data/ansible/roles/puma/meta/main.yml +0 -5
  92. data/ansible/roles/sidekiq/meta/main.yml +0 -5
  93. data/template/provision/ansible.cfg.erb +0 -9
  94. data/template/provision/group_vars/all.erb +0 -17
  95. data/template/provision/group_vars/template.erb +0 -11
  96. data/template/provision/host_vars/template.erb +0 -4
@@ -3,76 +3,113 @@ require 'erb'
3
3
  require 'securerandom'
4
4
  class Subspace::Commands::Init < Subspace::Commands::Base
5
5
  def initialize(args, options)
6
- if args.first == "vars"
7
- init_vars
8
- else
9
- run
6
+ options.default env: "dev"
7
+
8
+ @env = options.env
9
+ @template = options.template
10
+
11
+ if options.ansibe.nil? && options.terraform.nil?
12
+ # They didn't pass in any options (subspace init) so just do both
13
+ options.ansible = true
14
+ options.terraform = true
15
+ end
16
+
17
+ if @template.nil? && options.terraform == true
18
+ answer = ask "What template/server configuration would you like to use? e.g. 'workhorse' or 'oxenwagen'"
19
+ @template = answer
10
20
  end
21
+
22
+ @init_ansible = options.ansible
23
+ @init_terraform = options.terraform
24
+ run
11
25
  end
12
26
 
13
27
  def run
14
28
  if File.exists? dest_dir
15
29
  answer = ask "Subspace appears to be initialized. Reply 'yes' to continue anyway: [no] "
16
30
  abort unless answer.chomp == "yes"
31
+ else
32
+ FileUtils.mkdir_p dest_dir
17
33
  end
18
34
 
19
- FileUtils.mkdir_p File.join dest_dir, "group_vars"
20
- FileUtils.mkdir_p File.join dest_dir, "host_vars"
21
- FileUtils.mkdir_p File.join dest_dir, "vars"
22
- FileUtils.mkdir_p File.join dest_dir, "roles"
35
+ init_pemfile
36
+ init_ansible if @init_ansible
37
+ init_terraform if @init_terraform
23
38
 
24
- copy ".gitignore"
25
- #template "../provision.rb"
26
- template "ansible.cfg"
27
- template "hosts"
28
- template "group_vars/all"
39
+ puts """
40
+ 1. Inspect key config files:
41
+ - config/subspace/terraform/#{@env}/main.tf # Main terraform file
42
+ - config/subspace/#{@env}.yml # Main ansible playbook
43
+ - config/subspace/group_vars # Ansible configuration options
44
+ - config/subspace/inventory.yml # Server Inventory
45
+ - config/subspace/templates/authorized_keys # SSH Authorized Keys
46
+ - config/subspace/templates/application.yml # Application Environment variables
29
47
 
30
- create_vault_pass
31
- environments.each do |env|
32
- @env = env
33
- @hostname = hostname(env)
34
- template "group_vars/template", "group_vars/#{env}"
35
- template "host_vars/template", "host_vars/#{env}"
36
- create_vars_file_for_env env
37
- template "playbook.yml", "#{env}.yml"
38
- end
39
- create_vars_file_for_env "development"
40
- init_vars
48
+ 2. create cloud infrastructure with terraform:
41
49
 
42
- puts """
43
- 1. Create a server.
50
+ subspace tf #{@env}
44
51
 
45
- 2. Set your server's location:
52
+ 3. Bootstrap the new server
46
53
 
47
- vim config/provision/host_vars/production
48
- vim config/provision/host_vars/dev
54
+ subspace boostrap #{@env}1
49
55
 
50
- 3. Set up your authorized_keys:
56
+ 4. Inspect new environment
57
+ - ensure the correct roles are present in #{@env}.yml
58
+ - Check ansible configuration variables in group_vars/#{@env}
51
59
 
52
- vim config/provision/authorized_keys
60
+ 4. Provision the new servers with ansible:
53
61
 
54
- 4. Then provision your server:
62
+ subspace provision #{@env}
55
63
 
56
- subspace provision dev
57
- subspace provision production
64
+ !!MAKE SURE YOU PUT config/subspace/subspace.pem SOMEWHERE!!
58
65
 
59
66
  """
60
67
 
61
68
  end
62
69
 
63
- def init_vars
64
- FileUtils.mkdir_p File.join dest_dir, "templates"
65
- copy "templates/application.yml.template"
70
+ private
71
+
72
+ def init_pemfile
73
+ pem = File.join dest_dir, "subspace.pem"
74
+ if File.exist?(pem)
75
+ say "Existing SSH Keypair exists. Skipping keygen."
76
+ else
77
+ say "Creating SSH Keypair in #{pem}"
78
+ `ssh-keygen -t rsa -f #{pem}`
79
+ end
66
80
  end
67
81
 
68
- private
82
+ def init_ansible
83
+ FileUtils.mkdir_p File.join dest_dir, "group_vars"
84
+ FileUtils.mkdir_p File.join dest_dir, "secrets"
85
+ FileUtils.mkdir_p File.join dest_dir, "roles"
86
+ FileUtils.mkdir_p File.join dest_dir, "templates"
87
+
88
+ copy ".gitignore"
89
+ template "ansible.cfg"
90
+ template "group_vars/all"
91
+ template "inventory.yml"
92
+ template "templates/authorized_keys"
69
93
 
70
- def project_name
71
- File.basename(Dir.pwd)
94
+ create_vault_pass
95
+ @hostname = hostname(@env)
96
+ create_secrets_for @env
97
+ template "group_vars/template", "group_vars/#{@env}"
98
+ template "playbook.yml", "#{@env}.yml"
99
+
100
+ create_secrets_for "development" #TODO rename to local?
101
+ init_appyml
72
102
  end
73
103
 
74
- def environments
75
- %w(production dev)
104
+ def init_terraform
105
+ Subspace::Commands::Terraform.ensure_terraform
106
+ Subspace::Commands::Terraform.check_aws_credentials(project_name)
107
+
108
+ FileUtils.mkdir_p File.join dest_dir, "terraform", @env
109
+
110
+ set_latest_ami
111
+ template "terraform/template/main-#{@template}.tf", "terraform/#{@env}/main.tf"
112
+ copy "terraform/.gitignore"
76
113
  end
77
114
 
78
115
  def hostname(env)
@@ -87,10 +124,22 @@ class Subspace::Commands::Init < Subspace::Commands::Base
87
124
  end
88
125
  end
89
126
 
90
- def create_vars_file_for_env(env)
91
- template "vars/template", "vars/#{env}.yml"
127
+ def create_secrets_for(env)
128
+ template "secrets/template", "secrets/#{env}.yml"
92
129
  Dir.chdir dest_dir do
93
- `ansible-vault encrypt vars/#{env}.yml`
130
+ `ansible-vault encrypt secrets/#{env}.yml`
94
131
  end
95
132
  end
133
+
134
+ def init_appyml
135
+ copy "templates/application.yml.template"
136
+ end
137
+
138
+ def set_latest_ami
139
+ @latest_ami = `aws --profile subspace-#{project_name} ec2 describe-images \
140
+ --filters 'Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64*' \
141
+ --query 'Images[*].[ImageId,CreationDate]' --output text \
142
+ | sort -k2 -r \
143
+ | head -n1 | cut -f1`.chomp
144
+ end
96
145
  end
@@ -0,0 +1,45 @@
1
+ class Subspace::Commands::Inventory < Subspace::Commands::Base
2
+
3
+ def initialize(args, options)
4
+ command = args.first
5
+ @env = options.env
6
+ case command
7
+ when "capistrano"
8
+ capistrano_deployrb
9
+ when "list"
10
+ list_inventory
11
+ else
12
+ say "Unknown or missing command to inventory: #{command}"
13
+ say "try subspace inventory [list, capistrano]"
14
+ end
15
+ end
16
+
17
+ def list_inventory
18
+ inventory.find_hosts!(@env || "all").each do |host_name|
19
+ host = inventory.hosts[host_name]
20
+ puts "#{host.name}\t#{host.vars["ansible_host"]}\t(#{host.group_list.join ','})"
21
+ end
22
+ end
23
+
24
+ def capistrano_deployrb
25
+ if @env.nil?
26
+ puts "Please provide an environment e.g: subspace inventory capistrano --env production"
27
+ exit
28
+ end
29
+
30
+ say "# config/deploy/#{@env}.rb"
31
+ say "# Generated by Subspace"
32
+ inventory.find_hosts!(@env).each do |host_name|
33
+ host = inventory.hosts[host_name]
34
+ db_role = false
35
+ roles = host.group_list.map do |group_name|
36
+ if group_name =~ /web/
37
+ ["web", "app"]
38
+ elsif group_name =~ /worker/
39
+ ["app", db_role ? nil : "db"]
40
+ end
41
+ end.compact.uniq
42
+ say "server '#{host.vars["ansible_host"]}', user: 'deploy', roles: %w{#{roles.join(' ')}} # #{host.vars["hostname"]}"
43
+ end
44
+ end
45
+ end
@@ -11,6 +11,6 @@ class Subspace::Commands::Maintain < Subspace::Commands::Base
11
11
  def run
12
12
  ansible_options = ["--diff", "--tags=maintenance"]
13
13
  ansible_options = ansible_options | pass_through_params
14
- ansible_command "ansible-playbook", "#{@environment}.yml", *ansible_options
14
+ ansible_playbook "#{@environment}.yml", *ansible_options
15
15
  end
16
16
  end
@@ -9,8 +9,6 @@ class Subspace::Commands::Provision < Subspace::Commands::Base
9
9
  end
10
10
 
11
11
  def run
12
- ansible_options = ["--diff"]
13
- ansible_options = ansible_options | pass_through_params
14
- ansible_command "ansible-playbook", "#{@environment}.yml", *ansible_options
12
+ ansible_playbook "#{@environment}.yml", *pass_through_params
15
13
  end
16
14
  end
@@ -1,4 +1,4 @@
1
- class Subspace::Commands::Vars < Subspace::Commands::Base
1
+ class Subspace::Commands::Secrets < Subspace::Commands::Base
2
2
  def initialize(args, options)
3
3
  @environment = args.first
4
4
  @action = if options.edit
@@ -13,13 +13,14 @@ class Subspace::Commands::Vars < Subspace::Commands::Base
13
13
  end
14
14
 
15
15
  def run
16
+ update_ansible_cfg
16
17
  case @action
17
18
  when "create"
18
19
  create_local
19
20
  when "view", "edit"
20
- ansible_command "ansible-vault", @action, "vars/#{@environment}.yml"
21
+ ansible_command "ansible-vault", @action, "secrets/#{@environment}.yml"
21
22
  else
22
- abort "Invalid vars command"
23
+ abort "Invalid secrets command"
23
24
  end
24
25
  end
25
26
 
@@ -30,7 +31,7 @@ class Subspace::Commands::Vars < Subspace::Commands::Base
30
31
  end
31
32
  src = application_yml_template
32
33
  dest = "config/application.yml"
33
- vars_file = File.join(project_path, "config/provision/vars/#{@environment}.yml")
34
+ vars_file = File.join(project_path, dest_dir, "/secrets/#{@environment}.yml")
34
35
  extra_vars = "project_path=#{project_path} vars_file=#{vars_file} src=#{src} dest=#{dest}"
35
36
  ansible_command "ansible-playbook", File.join(playbook_dir, "local_template.yml"), "--extra-vars", extra_vars
36
37
  say "File created at config/application.yml with #{@environment} secrets"
@@ -42,7 +43,7 @@ class Subspace::Commands::Vars < Subspace::Commands::Base
42
43
  private
43
44
 
44
45
  def application_yml_template
45
- "config/provision/templates/application.yml.template"
46
+ "#{dest_dir}/templates/application.yml.template"
46
47
  end
47
48
 
48
49
  end
@@ -1,4 +1,5 @@
1
1
  require 'yaml'
2
+ require 'subspace/inventory'
2
3
  class Subspace::Commands::Ssh < Subspace::Commands::Base
3
4
  PASS_THROUGH_PARAMS = ["i"]
4
5
 
@@ -10,18 +11,19 @@ class Subspace::Commands::Ssh < Subspace::Commands::Base
10
11
  end
11
12
 
12
13
  def run
13
- if !File.exists? "config/provision/host_vars/#{@host}"
14
+ if !inventory.hosts[@host]
14
15
  say "No host '#{@host}' found. "
15
- all_hosts = Dir["config/provision/host_vars/*"].collect {|f| File.basename(f) }
16
+ all_hosts = inventory.hosts.keys
16
17
  say (["Available hosts:"] + all_hosts).join("\n\t")
17
18
  return
18
19
  end
19
- host_vars = YAML.load_file("config/provision/host_vars/#{@host}")
20
- user = @user || host_vars["ansible_ssh_user"] || host_vars["ansible_user"]
21
- host = host_vars["ansible_ssh_host"] || host_vars["ansible_host"]
22
- port = host_vars["ansible_ssh_port"] || host_vars["ansible_port"] || 22
23
- cmd = "ssh #{user}@#{host} -p #{port} #{pass_through_params.join(" ")}"
24
- say cmd
20
+ host_vars = inventory.hosts[@host].vars
21
+ user = host_vars["ansible_user"]
22
+ host = host_vars["ansible_host"]
23
+ port = host_vars["ansible_port"] || 22
24
+ pem = host_vars["ansible_ssh_private_key_file"] || 'subspace.pem'
25
+ cmd = "ssh #{user}@#{host} -p #{port} -i config/subspace/#{pem} #{pass_through_params.join(" ")}"
26
+ say "> #{cmd} \n"
25
27
  exec cmd
26
28
  end
27
29
  end
@@ -0,0 +1,83 @@
1
+ require 'json'
2
+ module Subspace
3
+ module Commands
4
+ class Terraform < Subspace::Commands::Base
5
+
6
+ def self.check_aws_credentials(project_name)
7
+ ENV["AWS_ACCESS_KEY_ID"] = nil
8
+ ENV["AWS_SECRET_ACCESS_KEY"] = nil
9
+
10
+ profile = "subspace-#{project_name}"
11
+
12
+ system("aws --profile #{profile} configure list &> /dev/null ")
13
+ if $? != 0
14
+ puts "No AWS Profile '#{profile}' configured. Please enter your credentials."
15
+ system("aws --profile #{profile} configure")
16
+ system("aws --profile #{profile} configure list &> /dev/null ")
17
+ if $? != 0
18
+ puts "FATAL: could not configure aws. Please try again"
19
+ exit
20
+ end
21
+ else
22
+ puts "Using AWS Profile #{profile}"
23
+ end
24
+ true
25
+ end
26
+
27
+ def self.ensure_terraform
28
+ if `terraform -v --json | jq -r .terraform_version` =~ /1\.\d+/
29
+ puts "Terraform found."
30
+ return true
31
+ else
32
+ puts "Please install terraform at least 1.1 locally"
33
+ return false
34
+ end
35
+ end
36
+
37
+ def initialize(args, options)
38
+ @env = args.shift
39
+ @args = args
40
+ @options = options
41
+ run
42
+ end
43
+
44
+ def run
45
+ self.class.check_aws_credentials(project_name) or exit
46
+ self.class.ensure_terraform or exit
47
+ if @args.any?
48
+ terraform_command(@args.shift, *@args)
49
+ else
50
+ puts "No command specified, running plan/apply"
51
+ terraform_command("init", "-upgrade") or return
52
+ terraform_command("apply") or return
53
+ update_inventory
54
+ end
55
+ end
56
+
57
+ private
58
+ def terraform_command(command, *args)
59
+ result = nil
60
+ # update_terraformrc
61
+ Dir.chdir "config/subspace/terraform/#{@env}" do
62
+ say ">> Running terraform #{command} #{args.join(' ')}"
63
+ result = system("terraform", command, *args, out: $stdout, err: $stderr)
64
+ say "<< Done"
65
+ end
66
+ result
67
+ end
68
+
69
+ def update_terraformrc
70
+ template! "terraformrc", ".terraformrc"
71
+ end
72
+
73
+ def update_inventory
74
+ puts "Apply succeeded, updating inventory."
75
+ Dir.chdir "config/subspace/terraform/#{@env}" do
76
+ @output = JSON.parse `terraform output -json oxenwagen`
77
+ end
78
+ inventory.merge(@output)
79
+ inventory.write
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,144 @@
1
+ require 'yaml'
2
+ module Subspace
3
+ class Inventory
4
+ attr_accessor :group_vars, :hosts, :global_vars, :path
5
+ def initialize
6
+ @hosts = {}
7
+ @group_vars = {}
8
+ @global_vars = {}
9
+ end
10
+
11
+ # Find all the hosts in the host/group or exit
12
+ def find_hosts!(host_spec)
13
+ if self.groups[host_spec]
14
+ return self.groups[host_spec].host_list.map { |m| self.hosts[m] }
15
+ elsif self.hosts[host_spec]
16
+ return [self.hosts[host_spec]]
17
+ else
18
+ say "No inventory matching: '#{host_spec}' found. "
19
+ say (["Available hosts:"] + self.hosts.keys).join("\n\t")
20
+ say (["Available groups:"] + self.groups.keys).join("\n\t")
21
+ exit
22
+ end
23
+ end
24
+
25
+ def self.read(path)
26
+ inventory = new
27
+ inventory.path = path
28
+
29
+ yml = YAML.load(File.read(path)).to_h
30
+
31
+ # Run through all hosts
32
+ yml["all"]["hosts"].each do |name, vars|
33
+ inventory.hosts[name] = Host.new(name, vars: vars || {})
34
+ end
35
+
36
+ # Run through all children (groups)
37
+ # This does NOT handle sub-groups yet
38
+ yml["all"]["children"].each do |name, group|
39
+ next unless group["hosts"]
40
+
41
+ # Each group defines its host membership
42
+ group["hosts"].each do |host, vars|
43
+ inventory.hosts[host] ||= Host.new(host, vars: vars || {})
44
+ inventory.hosts[host].group_list.push name
45
+ end
46
+
47
+ if group["vars"]
48
+ inventory.group_vars[name] = group["vars"]
49
+ end
50
+ end
51
+
52
+ # Capture global variables
53
+ inventory.global_vars = yml["all"]["vars"] || {}
54
+
55
+ inventory
56
+ end
57
+
58
+ def write(path=nil)
59
+ File.write(@path || path, self.to_yml)
60
+ end
61
+
62
+ def merge(inventory_json)
63
+ inventory_json["inventory"]["hostnames"].each_with_index do |host, i|
64
+ if hosts[host]
65
+ old_ip = hosts[host].vars["ansible_host"]
66
+ new_ip = inventory_json["inventory"]["ip_addresses"][i]
67
+ if old_ip != new_ip
68
+ say " * Host '#{host}' IP address changed! You may need to update the inventory! (#{old_ip} => #{new_ip})"
69
+ end
70
+ next
71
+ end
72
+ hosts[host] = Host.new(host)
73
+ hosts[host].vars["ansible_host"] = inventory_json["inventory"]["ip_addresses"][i]
74
+ hosts[host].vars["ansible_user"] = inventory_json["inventory"]["users"][i]
75
+ hosts[host].vars["hostname"] = host
76
+ hosts[host].group_list = inventory_json["inventory"]["groups"][i].split(/\s/)
77
+ end
78
+ end
79
+
80
+ def to_yml
81
+ all_groups = {}
82
+ all_hosts = {}
83
+ @hosts.each do |name, host|
84
+ all_hosts[host.name] = host.vars.empty? ? nil : host.vars.transform_keys(&:to_s)
85
+ host.group_list.each do |group|
86
+ all_groups[group] ||= { "hosts" => {}}
87
+ all_groups[group]["hosts"][host.name] = nil
88
+ end
89
+ end
90
+
91
+ @group_vars.each do |group, vars|
92
+ all_groups[group] ||= {}
93
+ if !vars.empty?
94
+ all_groups[group]["vars"] = vars.transform_keys(&:to_s)
95
+ end
96
+ end
97
+
98
+ yml = {
99
+ "all" => {
100
+ "hosts" => all_hosts,
101
+ "children" => all_groups.empty? ? nil : all_groups
102
+ }
103
+ }
104
+
105
+ if !@global_vars.empty?
106
+ yml["all"]["vars"] = @global_vars.transform_keys(&:to_s)
107
+ end
108
+
109
+ YAML.dump(yml)
110
+ end
111
+
112
+ def groups
113
+ @groups ||= begin
114
+ all_groups = {"all" => Group.new("all", vars: {}, host_list: hosts.keys) }
115
+ @hosts.each do |name, host|
116
+ host.group_list.each do |group|
117
+ all_groups[group] ||= Group.new(group, vars: @group_vars[group])
118
+ all_groups[group].host_list.append(name)
119
+ end
120
+ end
121
+ all_groups
122
+ end
123
+ end
124
+
125
+ class Host
126
+ attr_accessor :vars, :group_list, :name
127
+
128
+ def initialize(name, vars: {}, group_list: [])
129
+ @name = name
130
+ @vars = vars
131
+ @group_list = group_list
132
+ end
133
+ end
134
+
135
+ class Group
136
+ attr_accessor :name, :vars, :host_list
137
+ def initialize(name, vars: {}, host_list: [])
138
+ @name = name
139
+ @vars = vars
140
+ @host_list = host_list
141
+ end
142
+ end
143
+ end
144
+ end
@@ -1,3 +1,3 @@
1
1
  module Subspace
2
- VERSION = "2.5.7"
2
+ VERSION = "3.0.0.rc1"
3
3
  end
data/subspace.gemspec CHANGED
@@ -13,6 +13,13 @@ Gem::Specification.new do |spec|
13
13
  spec.description = %q{WIP -- don't use this :)}
14
14
  spec.homepage = "https://github.com/tenforwardconsulting/subspace"
15
15
  spec.license = "MIT"
16
+ spec.post_install_message = <<~EOS
17
+ *** Subspace 3 has many breaking changes
18
+ Primarily, the entire configuration directory structure has moved from config/provision to config/subspace.
19
+ You will need to migrate your old configuration to the new location, or downgrade to Subspace 2 if this was not intentional.
20
+ Please review the Upgrade guide: https://github.com/tenforwardconsulting/subspace/UPGRADING.md
21
+ EOS
22
+
16
23
 
17
24
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
25
  # to allow pushing to a single host or delete this section to allow pushing to any host.
@@ -24,7 +31,7 @@ Gem::Specification.new do |spec|
24
31
 
25
32
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
33
  spec.bindir = "exe"
27
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
+ spec.executables << "subspace"
28
35
  spec.require_paths = ["lib"]
29
36
 
30
37
  spec.add_development_dependency "bundler", "~> 2.1"
@@ -1,3 +1,6 @@
1
1
  .vault_pass
2
2
  ansible.cfg
3
3
  *.retry
4
+ *.tfstate*
5
+ *.pem
6
+ .terraform
@@ -0,0 +1,16 @@
1
+ [defaults]
2
+ forks = 10
3
+ roles_path = ./roles:<%= File.join(gem_path, 'ansible', 'roles') %>:/etc/ansible/roles
4
+ vault_password_file = .vault_pass
5
+ inventory = inventory.yml
6
+ # Uncomment to add timestamps to tasks to find slow ones.
7
+ # callback_whitelist = profile_tasks
8
+
9
+ <% if @mitogen_path %>
10
+ strategy_plugins = <%= @mitogen_path %>/ansible_mitogen/plugins/strategy
11
+ strategy = mitogen_linear
12
+ <% end %>
13
+
14
+ [ssh_connection]
15
+ pipelining = True
16
+ control_path = /tmp/subspace-control-%%h-%%p-%%r
@@ -0,0 +1,28 @@
1
+ # Conventions and defaults are defined here
2
+ project_name: <%= project_name %>
3
+ database_host: localhost
4
+ use_sudo: true
5
+
6
+ # ruby-common
7
+ # pull the checksum/url from https://www.ruby-lang.org/en/downloads/
8
+ ruby_version: # ruby-2.7.1
9
+ ruby_checksum: # d418483bdd0000576c1370571121a6eb24582116db0b7bb2005e90e250eae418
10
+ ruby_download_location: # https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.1.tar.gz
11
+ bundler_version: # 2.3.5
12
+
13
+ # Other stuff
14
+ letsencrypt_email:
15
+ nodejs_version: # 16.x
16
+ ssl_enabled: true
17
+ postgresql_version: # 14
18
+
19
+ logrotate_scripts:
20
+ - name: rails
21
+ path: "/u/apps/{{project_name}}/shared/log/{{rails_env}}.log"
22
+ options:
23
+ - weekly
24
+ - size 100M
25
+ - missingok
26
+ - compress
27
+ - delaycompress
28
+ - copytruncate
@@ -0,0 +1,26 @@
1
+ ## rails
2
+ rails_env: <%= @env %>
3
+
4
+ database_pool: 5
5
+ database_name: "{{project_name}}_{{rails_env}}"
6
+ database_user: "{{project_name}}"
7
+ database_adapter: postgresql
8
+ # job_queues:
9
+ # - default
10
+ # - mailers
11
+
12
+ # nginx / letsencrypt
13
+ server_name: "{{ansible_host}}"
14
+
15
+ ## postgresql
16
+ # postgresql_version: 14
17
+ # postgresql_authentication:
18
+ # - type: local
19
+ # user: "{{database_user}}"
20
+ # database: 'all'
21
+ # method: trust
22
+
23
+ ## puma
24
+ puma_workers: 1
25
+ puma_min_threads: 4
26
+ puma_max_threads: 16
File without changes
@@ -0,0 +1,11 @@
1
+ ---
2
+ all:
3
+ hosts:
4
+ <%= @env %>1:
5
+ ansible_host: localhost
6
+ ansible_user: ubuntu
7
+ ansible_ssh_private_key_file: subspace.pem
8
+ hostname: web1
9
+ children:
10
+ <%= @env %>:
11
+ <%= @env %>1: