vps 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,96 @@
1
+ module VPS
2
+ class CLI < Thor
3
+ class Service < Thor
4
+
5
+ SERVICES = "#{VPS::ROOT}/config/services.yml"
6
+
7
+ desc "add HOST [SERVICE]", "Add service to host configuration"
8
+ def add(host, service = nil)
9
+ config = VPS.read_config(host)
10
+
11
+ unless service
12
+ list = services.keys.sort - config[:services].keys
13
+ service = list[Ask.list("Which service to you want to add?", list)]
14
+ end
15
+
16
+ if !config[:services].include?(service) && (yml = services[service])
17
+ list = tags(yml[:image] || "library/#{service}")
18
+ tag = list[Ask.list("Choose which tag to use", list)]
19
+ image = "#{yml[:image] || service}:#{tag}"
20
+
21
+ yml, volumes = finalize_config(yml)
22
+ config[:services][service] = {:image => image}.merge(yml)
23
+ config[:volumes].concat(volumes).uniq!
24
+
25
+ VPS.write_config(host, config)
26
+ end
27
+ end
28
+
29
+ desc "remove HOST [SERVICE]", "Remove service from host configuration"
30
+ def remove(host, service = nil)
31
+ config = VPS.read_config(host)
32
+
33
+ unless service
34
+ list = config[:services].keys.sort
35
+ service = list[Ask.list("Which service do you want to remove?", list)]
36
+ end
37
+
38
+ if config[:services].delete(service)
39
+ VPS.write_config(host, config)
40
+ end
41
+ end
42
+
43
+ desc "list HOST", "List services of host configuration"
44
+ def list(host)
45
+ config = VPS.read_config(host)
46
+
47
+ services = config[:services].collect do |service, yml|
48
+ "* #{yml[:image]}"
49
+ end.sort
50
+
51
+ puts services
52
+ end
53
+
54
+ private
55
+
56
+ def services
57
+ @services ||= with_indifferent_access(YAML.load_file(SERVICES))
58
+ end
59
+
60
+ def tags(image)
61
+ uri = URI.parse("https://registry.hub.docker.com/v2/repositories/#{image}/tags/")
62
+ tags = JSON.parse(Net::HTTP.get(uri))["results"]
63
+ tags.collect{|tag| tag["name"]}.sort{|a, b| (a == "latest") ? -1 : (b <=> a)}
64
+ end
65
+
66
+ def with_indifferent_access(object)
67
+ case object
68
+ when Hash
69
+ object.inject({}.with_indifferent_access) do |hash, (key, value)|
70
+ hash[key] = with_indifferent_access(value)
71
+ hash
72
+ end
73
+ when Array
74
+ object.collect{|item| with_indifferent_access(item)}
75
+ else
76
+ object
77
+ end
78
+ end
79
+
80
+ def finalize_config(config)
81
+ config.delete(:image)
82
+ if config[:environment]
83
+ config[:environment] = config[:environment].inject({}) do |env, variable|
84
+ env[variable] = Ask.input(variable)
85
+ env
86
+ end
87
+ end
88
+ volumes = (config[:volumes] || []).collect do |volume|
89
+ volume.split(":")[0]
90
+ end
91
+ [config, volumes]
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,95 @@
1
+ module VPS
2
+ class CLI < Thor
3
+ class Upstream < Thor
4
+
5
+ desc "add HOST[:UPSTREAM] PATH", "Add upstream to host configuration"
6
+ def add(host_and_optional_upstream, path)
7
+ host, name = host_and_optional_upstream.split(":")
8
+ config = VPS.read_config(host)
9
+ path = File.expand_path(path)
10
+
11
+ unless config[:upstreams].any?{|upstream| upstream[:name] == name}
12
+ spec = derive_upstream(path)
13
+ spec[:nginx] ||= {
14
+ :http => nil,
15
+ :https => nil
16
+ }
17
+ config[:upstreams].push(spec.merge({
18
+ :name => name || File.basename(path),
19
+ :path => path,
20
+ :domains => [],
21
+ :email => nil,
22
+ :compose => nil
23
+ }))
24
+ VPS.write_config(host, config)
25
+ end
26
+ end
27
+
28
+ desc "remove HOST[:UPSTREAM]", "Remove upstream from host configuration"
29
+ def remove(host_and_optional_upstream)
30
+ host, name = host_and_optional_upstream.split(":")
31
+ config = VPS.read_config(host)
32
+
33
+ unless name
34
+ list = config[:upstreams].collect{|upstream| upstream[:name]}.sort
35
+ name = list[Ask.list("Which upstream do you want to remove?", list)]
36
+ end
37
+
38
+ if config[:upstreams].reject!{|upstream| upstream[:name] == name}
39
+ VPS.write_config(host, config)
40
+ end
41
+ end
42
+
43
+ desc "list HOST", "List upstreams of host configuration"
44
+ def list(host)
45
+ config = VPS.read_config(host)
46
+
47
+ upstreams = config[:upstreams].collect do |upstream|
48
+ "* #{upstream[:name]} (#{upstream[:path].gsub(Dir.home, "~")})"
49
+ end.sort
50
+
51
+ puts upstreams
52
+ end
53
+
54
+ private
55
+
56
+ def derive_upstream(path)
57
+ if Dir["#{path}/mix.exs"].any?
58
+ elixir = <<-ELIXIR
59
+ Application.started_applications()
60
+ |> Enum.reduce([], fn {name, _desc, _version}, acc ->
61
+ if(name in [:phoenix, :plug], do: [name | acc], else: acc)
62
+ end)
63
+ |> Enum.sort()
64
+ |> Enum.at(0)
65
+ |> IO.puts()
66
+ ELIXIR
67
+ type = `cd #{path} && mix run -e "#{elixir.strip.gsub(/\n\s+/, " ")}" | tail -n 1`.strip
68
+ {
69
+ type: type,
70
+ elixir_version: `cd #{path} && mix run -e "System.version() |> IO.puts()" | tail -n 1`.strip,
71
+ port: (type == "phoenix") ? 4000 : `cd #{path} && mix run -e ":ranch.info |> hd() |> elem(0) |> :ranch.get_port() |> IO.puts()" | tail -n 1`.strip.to_i
72
+ }
73
+ elsif Dir["#{path}/Gemfile"].any?
74
+ lines = `cd #{path} && BUNDLE_GEMFILE=#{path}/Gemfile bundle list`.split("\n")
75
+ type = %w(rails rack).detect{|gem| lines.any?{|line| line.include?("* #{gem} (")}}
76
+ {
77
+ type: type,
78
+ ruby_version: `$SHELL -l -c 'cd #{path} && ruby -e "puts RUBY_VERSION"'`.strip,
79
+ bundler_version: `$SHELL -l -c 'cd #{path} && bundle -v'`.split.last,
80
+ port: (type == "rails" ? 3000 : 9292) # :'(
81
+ }.tap do |spec|
82
+ if type == "rails"
83
+ spec[:nginx] = {
84
+ root: "/opt/app/public",
85
+ try_files: true,
86
+ proxy_redirect: "off"
87
+ }
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,17 @@
1
+ class OpenStruct
2
+ def self.to_hash(object, hash = {})
3
+ case object
4
+ when OpenStruct then
5
+ object.each_pair do |key, value|
6
+ hash[key.to_s] = to_hash(value)
7
+ end
8
+ hash
9
+ when Array then
10
+ object.collect do |value|
11
+ to_hash(value)
12
+ end
13
+ else
14
+ object
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ class String
2
+
3
+ COLORS = {
4
+ :red => 31,
5
+ :green => 32,
6
+ :yellow => 33,
7
+ :blue => 34,
8
+ :magenta => 35,
9
+ :cyan => 36,
10
+ :white => 39,
11
+ :gray => 90
12
+ }
13
+
14
+ COLORS.keys.each do |color|
15
+ define_method color do
16
+ colorize color
17
+ end
18
+ end
19
+
20
+ def indent(n, string = " ")
21
+ split("\n").collect do |line|
22
+ line.empty? ? line : "#{string * n}#{line}"
23
+ end.join("\n")
24
+ end
25
+
26
+ private
27
+
28
+ def colorize(color)
29
+ color = COLORS[color]
30
+ "\033[0;#{color}m#{self}\033[0m"
31
+ end
32
+
33
+ end
@@ -0,0 +1,7 @@
1
+ module VPS
2
+ MAJOR = 0
3
+ MINOR = 2
4
+ TINY = 0
5
+
6
+ VERSION = [MAJOR, MINOR, TINY].join(".")
7
+ end
@@ -0,0 +1,43 @@
1
+ ---
2
+ description:
3
+ Deploy web application to the server
4
+ usage:
5
+ deploy HOST [TOOL]
6
+ arguments:
7
+ - host
8
+ - tool
9
+ tasks:
10
+ - task: ensure
11
+ argument: user
12
+ fallbacks:
13
+ - task: read_config
14
+ key: user
15
+ - description: Obtaining remote user
16
+ task: remote_execute
17
+ command: whoami
18
+ - task: ensure
19
+ argument: tool
20
+ fallbacks:
21
+ - task: read_config
22
+ key: tool
23
+ - description: Choose which deploy tool to use
24
+ task: select
25
+ question: Which deployment tool do you want to use?
26
+ options: << playbooks >>
27
+ - task: ensure
28
+ argument: release_path
29
+ fallbacks:
30
+ - task: read_config
31
+ key: release_path
32
+ - description: Specify which directory to deploy to
33
+ task: input
34
+ question: Enter release path on the server
35
+ default: ~/app
36
+ - task: write_config
37
+ config:
38
+ user: << user >>
39
+ tool: << tool >>
40
+ release_path: << release_path >>
41
+ - task: obtain_config
42
+ - task: playbook
43
+ playbook: deploy/{{ tool }}
@@ -0,0 +1,96 @@
1
+ ---
2
+ source:
3
+ - http://dimafeng.com/2015/10/17/docker-distribution/
4
+ constants:
5
+ config_path: ~/.vps/{{ host }}
6
+ docker_compose_file: "{{ config_path }}/docker-compose.yml"
7
+ nginx_conf_file: "{{ config_path }}/data/nginx/app.conf"
8
+ tasks:
9
+ - task: loop
10
+ through: << upstreams >>
11
+ as: upstream
12
+ run:
13
+ - task: generate_file
14
+ template: docker/upstream/Dockerfile.{{ upstream.type }}.erb
15
+ target: "{{ config_path }}/{{ upstream.name }}/Dockerfile"
16
+ - description: Building '{{ upstream.name }}' image
17
+ task: execute
18
+ command: docker build --no-cache -f {{ config_path }}/{{ upstream.name }}/Dockerfile -t {{ upstream.name }} {{ upstream.path }}
19
+ - description: Saving docker images as .gzip file
20
+ task: loop
21
+ through: << upstreams >>
22
+ as: upstream
23
+ run:
24
+ - task: execute
25
+ command: docker save {{ upstream.name }} | gzip > /tmp/{{ upstream.name }}.gzip
26
+ - description: Uploading images
27
+ task: loop
28
+ through: << upstreams >>
29
+ as: upstream
30
+ run:
31
+ - task: upload
32
+ file: /tmp/{{ upstream.name }}.gzip
33
+ - description: Stopping current containers
34
+ task: remote_execute
35
+ command: cd {{ release_path }} && docker-compose stop
36
+ - description: Removing current containers
37
+ task: loop
38
+ through: << upstreams >>
39
+ as: upstream
40
+ run:
41
+ - task: remote_execute
42
+ command: docker container rm $(docker container ls -f ancestor={{ upstream.name }} -aq)
43
+ - description: Removing current images
44
+ task: loop
45
+ through: << upstreams >>
46
+ as: upstream
47
+ run:
48
+ - task: remote_execute
49
+ command: docker image rm {{ upstream.name }}
50
+ - task: run_tasks
51
+ tasks: << preload >>
52
+ - description: Unzipping and loading docker images
53
+ task: loop
54
+ through: << upstreams >>
55
+ as: upstream
56
+ run:
57
+ - task: remote_execute
58
+ command: gunzip < /tmp/{{ upstream.name }}.gzip | docker load
59
+ - task: generate_file
60
+ template: docker/docker-compose.yml.erb
61
+ target: "{{ docker_compose_file }}"
62
+ - description: Uploading docker-compose.yml
63
+ task: upload
64
+ file: "{{ docker_compose_file }}"
65
+ remote_path: "{{ release_path }}/docker-compose.yml"
66
+ - task: generate_file
67
+ template: docker/data/nginx/app.conf.erb
68
+ target: "{{ nginx_conf_file }}"
69
+ - description: Uploading Nginx config
70
+ task: upload
71
+ file: "{{ nginx_conf_file }}"
72
+ remote_path: "{{ release_path }}/data/nginx/app.conf"
73
+ - task: loop
74
+ through: << upstreams >>
75
+ as: upstream
76
+ run:
77
+ - task: generate_file
78
+ template: docker/upstream/init-letsencrypt.sh.erb
79
+ target: "{{ config_path }}/{{ upstream.name }}/init-letsencrypt.sh"
80
+ - description: Uploading Let’s Encrypt script
81
+ task: upload
82
+ file: "{{ config_path }}/{{ upstream.name }}/init-letsencrypt.sh"
83
+ remote_path: "{{ release_path }}/init-letsencrypt/{{ upstream.name }}.sh"
84
+ - description: Install SSL certificates (if required)
85
+ task: remote_execute
86
+ command:
87
+ - chmod +x {{ release_path }}/init-letsencrypt/{{ upstream.name }}.sh
88
+ - cd {{ release_path }} && if [ ! -d "data/certbot/conf/live/{{ domain:upstream }}" ]; then yes Y | sudo ./init-letsencrypt/{{ upstream.name }}.sh; fi
89
+ - description: Starting containers
90
+ task: remote_execute
91
+ command: cd {{ release_path }} && docker-compose up {{ up }} -d
92
+ - description: Checking running docker images
93
+ task: remote_execute
94
+ command: docker ps
95
+ - task: run_tasks
96
+ tasks: << postload >>
@@ -0,0 +1,12 @@
1
+ ---
2
+ description:
3
+ Execute an initial server setup
4
+ confirm:
5
+ Are you sure you want to initially setup your VPS?
6
+ constants:
7
+ user: root
8
+ arguments:
9
+ - host
10
+ tasks:
11
+ - task: playbook
12
+ playbook: init
@@ -0,0 +1,110 @@
1
+ ---
2
+ source:
3
+ - https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-18-04
4
+ - https://github.com/jasonheecs/ubuntu-server-setup/blob/master/setupLibrary.sh
5
+ - https://www.linode.com/stackscripts/view/1
6
+ - https://gist.github.com/parente/0227cfbbd8de1ce8ad05
7
+ tasks:
8
+ - description: Updating server with latest packages
9
+ task: remote_execute
10
+ command: apt-get update && apt-get upgrade -y
11
+ - description: Choose which tasks to execute
12
+ task: multiselect
13
+ question: Which tasks do you want to execute?
14
+ options:
15
+ set_server_hostname: Set server hostname
16
+ add_sudo_user: Add new user with sudo access
17
+ disable_password_authentication: Disable password authentication to the server
18
+ setup_uncomplicated_firewall: Setup Uncomplicated FireWall
19
+ setup_timezone: Setup server timezone
20
+ install_network_time_protocol: Install Network Time Protocol
21
+ deny_root_login: Deny root login to the server
22
+ - description: Setting the server hostname
23
+ task: when
24
+ boolean: set_server_hostname
25
+ run:
26
+ - task: input
27
+ question: Enter hostname for the server
28
+ as: hostname
29
+ - task: remote_execute
30
+ command:
31
+ - echo '{{ hostname }}' > /etc/hostname
32
+ - hostname -F /etc/hostname
33
+ - description: Creating new sudo user
34
+ task: when
35
+ boolean: add_sudo_user
36
+ run:
37
+ - task: input
38
+ question: Enter username for the new sudo user
39
+ as: username
40
+ - task: input
41
+ question: Enter location of your public SSH key
42
+ default: ~/.ssh/id_rsa.pub
43
+ as: public_key
44
+ - task: remote_execute
45
+ command: adduser --disabled-password --gecos '' {{ username }}
46
+ - description: Granting administrative privileges
47
+ task: remote_execute
48
+ command: usermod -aG sudo {{ username }}
49
+ - description: Copying public SSH key
50
+ task: execute
51
+ command: cat {{ public_key }}
52
+ as: public_key
53
+ - description: Installing public SSH key on server
54
+ task: remote_execute
55
+ user: "{{ username }}"
56
+ command:
57
+ - mkdir -p ~/.ssh
58
+ - touch ~/.ssh/authorized_keys
59
+ - echo {{{ public_key }}} >> ~/.ssh/authorized_keys
60
+ - chmod 700 ~/.ssh
61
+ - chmod 600 ~/.ssh/authorized_keys
62
+ - description: Disabling sudo password for new user
63
+ task: remote_execute
64
+ command:
65
+ - cp /etc/sudoers /etc/sudoers.bak
66
+ - sh -c 'echo "{{ username }} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers'
67
+ - description: Disabling password authentication
68
+ task: when
69
+ boolean: disable_password_authentication
70
+ run:
71
+ - task: remote_execute
72
+ command:
73
+ - sed -i 's/#*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config
74
+ - sed -i 's/#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
75
+ - service ssh restart
76
+ - description: Denying root login to the server
77
+ task: when
78
+ boolean: deny_root_login
79
+ run:
80
+ - task: remote_execute
81
+ command:
82
+ - sed -i 's/#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
83
+ - service ssh restart
84
+ - description: Setting up Uncomplicated FireWall
85
+ task: when
86
+ boolean: setup_uncomplicated_firewall
87
+ run:
88
+ - task: remote_execute
89
+ command:
90
+ - ufw allow OpenSSH
91
+ - ufw allow http
92
+ - ufw allow https
93
+ - yes y | ufw enable
94
+ - description: Setting up the server timezone
95
+ task: when
96
+ boolean: setup_timezone
97
+ run:
98
+ - task: remote_execute
99
+ command:
100
+ export timezone=`wget -qO - http://geoip.ubuntu.com/lookup | sed -n -e 's/.*<TimeZone>\(.*\)<\/TimeZone>.*/\1/p'` &&
101
+ timedatectl set-timezone $timezone &&
102
+ echo $timezone
103
+ - description: Installing Network Time Protocol
104
+ task: when
105
+ boolean: install_network_time_protocol
106
+ run:
107
+ - task: remote_execute
108
+ command:
109
+ - apt-get update
110
+ - apt-get --assume-yes install ntp