vps 0.2.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.
@@ -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