provisional 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c810f27c3308831e2c9a01e86c6c22bf0551b3f
4
+ data.tar.gz: 68f65d50762afd19268a49b27bc6343cb6ec3800
5
+ SHA512:
6
+ metadata.gz: 76ad7cea5ea16f9d07c4247ab36d7713454c305fd7e8eaf2fc2095d3280e474b770cd79509a846300d483ebfd1b0878316ea65e6166e076fc4a2a09e8465ee84
7
+ data.tar.gz: 0f19a20d5e488385ba6dcb78f6e25bb6999447e240f174065c31f3ed61d337b1ee8ca60c0a2524417c95330484c8baa458d0557e5ed726434d2d8ce700b354d4
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /config/
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in provisional.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Craig Buchek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,86 @@
1
+ Provisional
2
+ ===========
3
+
4
+ Provisional is a tool to manage and provision server images.
5
+ It is designed to make building immutable servers easier,
6
+ by automating the process of image creation.
7
+
8
+ Images are created based on other images (such as an AWS AMI).
9
+ You define scripts that are run to install and configure any required software.
10
+
11
+ You can then provision your images to deploy your application.
12
+
13
+ Provisional currently works only with Digital Ocean,
14
+ but can easily be extended to work with other providers.
15
+
16
+
17
+ ## Installation
18
+
19
+ You'll need Ruby (2.0 or later) installed on your local system.
20
+
21
+ Install Provisional with Rubygems:
22
+
23
+ ~~~ bash
24
+ gem install provisional
25
+ ~~~
26
+
27
+
28
+ ## Usage
29
+
30
+ Provisional is meant to be used from within your application development directory.
31
+
32
+ ~~~ bash
33
+ provisional init # Create config/provisional/config.yml
34
+ provisional image list # List images (including all versions)
35
+ provisional image create image-name # Create an image
36
+ provisional image update image-name # Update an image (or all images)
37
+ provisional deploy # Deploy all images, per config file
38
+ ~~~
39
+
40
+
41
+ ## Why Not Docker?
42
+
43
+ Docker is overly complex for simple application architectures.
44
+ There are a lot of coordination pieces (Fleet, Kubernetes, etc.) required.
45
+ We can also store our images with the provider instead of in our own Docker infrastructure.
46
+ That said, nothing prevents you from running Docker on top of the VM images you create with Provisional.
47
+ You might also be able to adapt Provisional to create Docker images.
48
+
49
+
50
+ ## Development
51
+
52
+ After checking out the repo, run `bin/setup` to install dependencies.
53
+ Then, run `bin/console` for an interactive prompt that will allow you to experiment.
54
+
55
+ To install this gem onto your local machine, run `bundle exec rake install`.
56
+ To release a new version, update the version number in `version.rb`,
57
+ and then run `bundle exec rake release` to create a git tag for the version,
58
+ push git commits and tags, and push the `.gem` file to [RubyGems].
59
+
60
+ To run the tests, you'll need a Digital Ocean account, with an API key generated.
61
+ You'll have to export the key in an environment variable named `DIGITAL_OCEAN_API_KEY`.
62
+ You can also enable extra debugging by exporting `GLI_DEBUG=true`.
63
+
64
+
65
+ ## Contributing
66
+
67
+ 1. Fork the [project repo].
68
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
69
+ 3. Make sure tests pass (`cucumber` and `rspec`).
70
+ 4. Commit your changes (`git commit -am 'Add some feature'`).
71
+ 5. Push to the branch (`git push origin my-new-feature`).
72
+ 6. Create a new [pull request].
73
+
74
+
75
+ ## TODO
76
+
77
+ Note that this is currently a work in progress. Much remains to be done.
78
+
79
+ - Refactor to put an OOP layer above the DropletKit layer.
80
+ - Move Digital Ocean support to an adapter.
81
+ - Add an adapter for AWS.
82
+
83
+
84
+ [project repo]: https://github.com/boochtek/ruby_preserves/fork
85
+ [pull request]: https://github.com/boochtek/ruby_preserves/pulls
86
+ [RubyGems]: https://rubygems.org
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "provisional"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ require "pry"
10
+ Pry.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+
3
+ apt-get update
4
+ apt-get upgrade
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ dpkg --list | grep ^rc | awk '{print $2}' | xargs dpkg --purge
4
+ apt-get purge portmap
5
+ apt-get autoremove
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ # It's nice to have man pages for kernel system calls and library functions.
4
+ apt-get install manpages-dev
5
+
6
+ # Socat is nice for making socket connections between systems.
7
+ apt-get install socat
@@ -0,0 +1,23 @@
1
+ #!/bin/sh
2
+
3
+
4
+ ### NTP ###
5
+
6
+ # We are running NTP to keep the clock accurate.
7
+
8
+
9
+ ## Installation
10
+
11
+ sudo apt-get install ntp ntp-doc ntpdate
12
+
13
+
14
+ ## Configuration
15
+
16
+ # We are using the default configuration that Debian ships with. This is primarily a client configuration – we allow other systems only to get the current time; they may not query any further information. (This is limited via the restrict keyword.) The daemon runs primarily in order to sync the system's time with the upstream NTP servers.
17
+
18
+ # The configuration file points to multiple upstream NTP servers within debian.pool.ntp.org.
19
+
20
+
21
+ ## TODO
22
+
23
+ # Need to test that NTP is working.
@@ -0,0 +1,63 @@
1
+ #!/bin/sh
2
+
3
+
4
+ ## Make sure sudo is installed
5
+ apt-get install sudo
6
+
7
+
8
+ ## Require Root Password
9
+
10
+ # By default, sudo requires a user to type in their own password in order to run a command.
11
+ # For added security, we prefer to use a different password to run commands as root.
12
+ # This way, if a user password is compromised, the attacker cannot run commands as root without additional work.
13
+
14
+ cat > /etc/sudoers.d/require_root_password << EOF
15
+ # Require root password (instead of the user's own password).
16
+ Defaults rootpw
17
+ EOF
18
+ chmod 440 /etc/sudoers.d/require_root_password
19
+ visudo -c -f /etc/sudoers.d/require_root_password
20
+
21
+
22
+ ## Environment
23
+
24
+ # The sudo command ensures that certain environment variables are not carried over, to prevent security problems.
25
+ # We need to tweak the set of environment variables a bit.
26
+
27
+ cat > /etc/sudoers.d/environment << EOF
28
+ # Set $HOME to the target user's home directory. Allows mysql clients to find root's $HOME/.my.cnf config file automatically.
29
+ Defaults always_set_home
30
+
31
+ # Reset all environment variables, except the ones we explicitly list.
32
+ Defaults env_reset
33
+ Defaults env_keep = "PATH MAIL PS1 PS2 HOSTNAME HISTSIZE \
34
+ LS_COLORS COLORS INPUTRC TZ \
35
+ LANG LC_ADDRESS LC_CTYPE LC_COLLATE LC_IDENTIFICATION \
36
+ LC_MEASUREMENT LC_MESSAGES LC_MONETARY LC_NAME LC_NUMERIC \
37
+ LC_PAPER LC_TELEPHONE LC_TIME LC_ALL LANGUAGE LINGUAS"
38
+ EOF
39
+ chmod 440 /etc/sudoers.d/environment
40
+ visudo -c -f /etc/sudoers.d/environment
41
+
42
+
43
+ ## Package Management
44
+
45
+ # Since installing and updating software from standard repositories is a common admin task with low security risk,
46
+ # we'll allow it without requiring a password.
47
+
48
+ touch /etc/sudoers.d/package_management
49
+ cat > /etc/sudoers.d/package_management << EOF
50
+ # Admin users may install and update software packages without having to supply a password.
51
+ Cmnd_Alias PACKAGE_INFO = /usr/bin/apt-get install *, /usr/bin/apt-get check, \
52
+ /usr/bin/apt-cache search *, /usr/bin/apt-cache show *, /usr/bin/apt-cache showpkg *, \
53
+ /usr/bin/aptitude search *, /usr/bin/aptitude show *, /usr/bin/aptitude changelog *
54
+ Cmnd_Alias PACKAGE_INSTALL = /usr/bin/apt-get install *, \
55
+ /usr/bin/aptitude install *, /usr/bin/aptitude reinstall *
56
+ Cmnd_Alias PACKAGE_UPDATE = /usr/bin/apt-get update, /usr/bin/apt-get upgrade, \
57
+ /usr/bin/aptitude update, /usr/bin/aptitude safe-upgrade
58
+ Cmnd_Alias PACKAGE_CLEAN = /usr/bin/apt-get autoremove, /usr/bin/apt-get clean, /usr/bin/apt-get autoclean, \
59
+ /usr/bin/aptitude clean, /usr/bin/aptitude autoclean
60
+ %sudo ALL = NOPASSWD: PACKAGE_INFO, PACKAGE_INSTALL, PACKAGE_UPDATE, PACKAGE_CLEAN
61
+ EOF
62
+ chmod 440 /etc/sudoers.d/package_management
63
+ visudo -c -f /etc/sudoers.d/package_management
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ apt-get install postgresql postgresql-doc postgresql-client
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ apt-get install haproxy
@@ -0,0 +1,45 @@
1
+ ---
2
+ vps:
3
+ provider: digital-ocean
4
+ api_key: <%= ENV["DIGITAL_OCEAN_API_KEY"] %>
5
+ defaults:
6
+ region: nyc3
7
+ disk-size: 20 GB
8
+ backups: true
9
+ private_networking: true
10
+ ipv6: true
11
+ locked: false
12
+ ssh_keys: all_ssh_keys
13
+ dns:
14
+ provider: dnsimple
15
+ api_key: <%= ENV["DNSIMPLE_API_KEY"] %>
16
+ domain: <%= ENV["DOMAIN"] %>
17
+ images:
18
+ base:
19
+ base-image: debian-8-x64
20
+ lb:
21
+ base-image: base
22
+ app:
23
+ base-image: base
24
+ db:
25
+ base-image: base
26
+ deployments:
27
+ staging:
28
+ lb:
29
+ servers: 1
30
+ cold-only: true
31
+ app:
32
+ servers: 2
33
+ db:
34
+ servers: 1
35
+ cold-only: true
36
+ www:
37
+ lb:
38
+ servers: 2
39
+ cold-only: true
40
+ app:
41
+ servers: 3
42
+ db:
43
+ servers: 2
44
+ cold-only: true
45
+ disk-size: 40 GB
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "provisional"
5
+ require "gli"
6
+
7
+
8
+ include GLI::App
9
+ subcommand_option_handling :normal
10
+ arguments :strict
11
+
12
+
13
+ program_desc "Manage and provision server images"
14
+ version Provisional::VERSION
15
+
16
+
17
+ desc "Configuration file"
18
+ flag [:c, :"config-file"], default_value: Provisional::CONFIG_FILE
19
+
20
+
21
+ desc "Create a Provisional config file for your project"
22
+ command :init do |init|
23
+ init.action do |global_options|
24
+ Provisional::Init.new(global_options).run
25
+ end
26
+ end
27
+
28
+
29
+ desc "Work with server images"
30
+ command :image do |image|
31
+ image.desc "List images"
32
+ image.command :list do |image_list|
33
+ image_list.action do |global_options, options, args|
34
+ Provisional::ImageOperations.new(global_options.merge(options)).list
35
+ end
36
+ end
37
+
38
+ image.desc "Build an image, based on another image, plus files and scripts"
39
+ image.arg "image_name"
40
+ image.command :build do |image_build|
41
+ image_build.action do |global_options, options, args|
42
+ image_name = args.first
43
+ Provisional::ImageOperations.new(global_options.merge(options)).build(image_name)
44
+ end
45
+ end
46
+ end
47
+
48
+
49
+ # desc "Work with servers"
50
+ # command :server do |server|
51
+ # server.desc "List servers"
52
+ # server.command :list do |server_list|
53
+ # server_list.action do |global_options, options, args|
54
+ # environment = args[0]
55
+ # options = global_options.merge(options)
56
+ # Provisional::Server.list(environment)
57
+ # end
58
+ # end
59
+ # end
60
+ #
61
+ #
62
+ desc "Deploy servers to a given environment"
63
+ arg_name "environment"
64
+ command :deploy do |deploy|
65
+ deploy.switch "cold", desc: "Perform a cold deploy, deploying all server types"
66
+ deploy.action do |global_options, options, args|
67
+ environment = args[0]
68
+ options = global_options.merge(options)
69
+ if options[:cold]
70
+ Provisional::Deployment.new(environment, options).deploy_cold
71
+ else
72
+ #Provisional::Deployment.new(environment, options).deploy_warm
73
+ end
74
+ end
75
+ end
76
+
77
+
78
+ exit run(ARGV)
@@ -0,0 +1,31 @@
1
+ require "provisional/version"
2
+ require "provisional/init"
3
+ require "provisional/image"
4
+ require "provisional/server"
5
+ require "provisional/deployment"
6
+ require "provisional/image_operations"
7
+ require "yaml"
8
+ require "erb"
9
+
10
+
11
+ module Provisional
12
+
13
+ CONFIG_DIRECTORY = "config/infrastructure"
14
+ CONFIG_FILE = "#{CONFIG_DIRECTORY}/provisional.yml"
15
+ DEFAULT_CONFIG = File.expand_path('../../data/default_config', __FILE__)
16
+
17
+ def self.config
18
+ @config ||= YAML.load(ERB.new(File.read(CONFIG_FILE)).result)
19
+ end
20
+
21
+ def self.digital_ocean
22
+ @digital_ocean ||= DropletKit::Client.new(access_token: digital_ocean_api_key)
23
+ end
24
+
25
+ private
26
+
27
+ def self.digital_ocean_api_key
28
+ @digital_ocean_api_key ||= Provisional.config["vps"]["api_key"]
29
+ end
30
+
31
+ end
@@ -0,0 +1,51 @@
1
+ require "droplet_kit"
2
+
3
+
4
+ class Provisional::Deployment
5
+
6
+ attr_reader :environment
7
+ attr_reader :options
8
+
9
+ def initialize(environment, options = {})
10
+ @environment = environment
11
+ @options = options
12
+ end
13
+
14
+ def deploy_cold
15
+ verify_all_required_images_have_been_built(environment)
16
+ puts "Deploying all images to '#{environment}'."
17
+ deployment_config.each do |server_type, server_config|
18
+ server_count = server_config["servers"]
19
+ image = Provisional::Image.find(name: server_type)
20
+ # For a cold deploy, we can assume (but should verify) that there are no existing servers in the environment.
21
+ # existing_servers = Provisional::Server.list(environment).reject{|server| server.name =~ /-\d{14}/}
22
+ # next_server_number = highest_server_number(existing_servers)
23
+ new_server_numbers = (1 .. server_count)
24
+ new_server_numbers.each do |server_number|
25
+ server_name = "#{server_type}#{server_number}.#{environment}"
26
+ puts "Create server #{server_name} with image '#{image.name}'"
27
+ Provisional::Server.create(name: server_name, image: image)
28
+ end
29
+ # existing_servers.each do |server|
30
+ # server.delete
31
+ # end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def verify_all_required_images_have_been_built(environment)
38
+ deployment_config.keys.each do |server_type|
39
+ raise "No image built for '#{server_type}'" if Provisional::Image.find(name: server_type).nil?
40
+ end
41
+ end
42
+
43
+ def deployment_config
44
+ Provisional.config["deployments"][environment] or raise "No deployments config file section for '#{environment}'"
45
+ end
46
+
47
+ def highest_server_number(server_names)
48
+ server_names.map{|name| name.split(".").first.gsub(/[^0-9]/, "").to_i}.sort.last
49
+ end
50
+
51
+ end
@@ -0,0 +1,43 @@
1
+ require "droplet_kit"
2
+
3
+
4
+ class Provisional::Image
5
+
6
+ def self.list
7
+ all.map(&:name)
8
+ end
9
+
10
+ def self.all
11
+ Provisional.digital_ocean.images.all.to_a # select{|image| image.type == "snapshot"}
12
+ end
13
+
14
+ def self.custom
15
+ all.select{|image| !image.public && image.name =~ /-\d{14}$/}
16
+ end
17
+
18
+ def self.find(options)
19
+ raise "Must pass in a hash" unless options.is_a?(Hash)
20
+ if options[:id]
21
+ Provisional.digital_ocean.images.find(id: id)
22
+ elsif options[:name]
23
+ name = options[:name]
24
+ image = all.select{|image| image.slug == name || image.name == name}.first
25
+ if image.nil?
26
+ image = all.select{|image| image.name =~ /#{name}-\d{14}/}.sort.last
27
+ end
28
+ return image
29
+ else
30
+ raise "Don't know how to find image with that criteria"
31
+ end
32
+ end
33
+
34
+ def self.create(options = {})
35
+ raise "Must pass in a hash" unless options.is_a?(Hash)
36
+ if options[:name] && options[:from]
37
+ # Create a new image from a server.
38
+ else
39
+ raise "Don't know how to create image with that criteria"
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,103 @@
1
+ require "droplet_kit"
2
+
3
+
4
+ class Provisional::ImageOperations
5
+
6
+ SSH_OPTIONS = "-o PasswordAuthentication=no -o StrictHostKeyChecking=no -o CheckHostIP=no -o VisualHostKey=no"
7
+
8
+ attr_reader :options
9
+
10
+ def initialize(options = {})
11
+ @options = options
12
+ end
13
+
14
+ def list
15
+ os_images.each do |image|
16
+ display(image)
17
+ end
18
+ custom_images.each do |image|
19
+ display(image)
20
+ end
21
+ end
22
+
23
+ def build(image_name)
24
+ puts "Base image for '#{image_name}' is '#{base_image_for(image_name)}'"
25
+ base_image = Provisional::Image.find(name: base_image_for(image_name))
26
+ name = "#{image_name}-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}"
27
+ server = Provisional::Server.create(name: name, image: base_image)
28
+ transfer_files_to_server(image_name, server)
29
+ run_scripts_on_server(image_name, server)
30
+ Provisional::Server.stop(server)
31
+ build_image_from_server(name, server.id)
32
+ Provisional::Server.delete(server)
33
+ end
34
+
35
+ def transfer_files_to_server(image_name, server)
36
+ ip_address = server.public_ip
37
+ wait_until_ssh_responds(ip_address)
38
+ %x(ssh #{SSH_OPTIONS} root@#{ip_address} mkdir -p /var/tmp/provisional)
39
+ %x(scp #{SSH_OPTIONS} -r #{Provisional::CONFIG_DIRECTORY}/#{image_name}/files/ root@#{ip_address}:/var/tmp/provisional/files/) if Dir.exists?("#{Provisional::CONFIG_DIRECTORY}/#{image_name}/files")
40
+ %x(scp #{SSH_OPTIONS} -r #{Provisional::CONFIG_DIRECTORY}/#{image_name}/scripts/ root@#{ip_address}:/var/tmp/provisional/scripts/) if Dir.exists?("#{Provisional::CONFIG_DIRECTORY}/#{image_name}/scripts")
41
+ %x(ssh #{SSH_OPTIONS} root@#{ip_address} chmod --silent 600 /var/tmp/provisional/files/*)
42
+ %x(ssh #{SSH_OPTIONS} root@#{ip_address} chmod --silent 700 /var/tmp/provisional/scripts/*)
43
+ end
44
+
45
+ def run_scripts_on_server(image_name, server)
46
+ ip_address = server.public_ip
47
+ wait_until_ssh_responds(ip_address)
48
+ %x(ssh #{SSH_OPTIONS} root@#{ip_address} run-parts --regex \\'.*\\' /var/tmp/provisional/scripts/)
49
+ end
50
+
51
+ def wait_until_ssh_responds(ip_address)
52
+ until %x(ssh #{SSH_OPTIONS} root@#{ip_address} "ls -d1 /") == "/\n"
53
+ puts "Waiting for SSH on #{ip_address} to respond"
54
+ sleep 1
55
+ end
56
+ end
57
+
58
+ def build_image_from_server(name, id)
59
+ action = Provisional.digital_ocean.droplet_actions.snapshot(droplet_id: id, name: name)
60
+ if action.is_a?(Hash) && action.has_key?("message")
61
+ raise "Error building image: #{message}"
62
+ end
63
+ action_id = action.id
64
+ print "Building '#{name}' image from server '#{id}'."
65
+ until Provisional.digital_ocean.actions.find(id: action_id).status == "completed"
66
+ putc(".")
67
+ $stdout.flush
68
+ sleep 5
69
+ end
70
+ puts "DONE"
71
+ end
72
+
73
+ def custom_images
74
+ @custom_images ||= all_images.select{|image| !image.public && image.name =~ /-\d{14}$/}
75
+ end
76
+
77
+ def all_images
78
+ @all_images ||= Provisional.digital_ocean.images.all.select{|image| image.type == "snapshot"}
79
+ end
80
+
81
+ private
82
+
83
+ def os_images
84
+ @os_images ||= all_images.select{|image| image.public}.select{|image| image.slug =~ /-\d/ || image.slug =~ /coreos/ }
85
+ end
86
+
87
+ def base_image_for(image_name)
88
+ image_config_for(image_name)["base-image"]
89
+ end
90
+
91
+ def image_config_for(image_name)
92
+ Provisional.config["images"][image_name] or raise "No config file section for image '#{image_name}'"
93
+ end
94
+
95
+ def display(image)
96
+ if image.public
97
+ puts "#{image.slug}: #{image.distribution} #{image.name}"
98
+ else
99
+ puts "#{image.name}"
100
+ end
101
+ end
102
+
103
+ end
@@ -0,0 +1,37 @@
1
+ require "fileutils"
2
+
3
+
4
+ class Provisional::Init
5
+
6
+ attr_reader :options
7
+
8
+ def initialize(options)
9
+ @options = options
10
+ end
11
+
12
+ def run
13
+ create_config_file
14
+ end
15
+
16
+ private
17
+
18
+ def create_config_file
19
+ unless File.exist?(config_file_directory)
20
+ FileUtils.mkdir_p(config_file_directory_parent)
21
+ FileUtils.copy_entry(Provisional::DEFAULT_CONFIG, config_file_directory_parent)
22
+ end
23
+ end
24
+
25
+ def config_file
26
+ options["config-file"]
27
+ end
28
+
29
+ def config_file_directory
30
+ File.dirname(config_file)
31
+ end
32
+
33
+ def config_file_directory_parent
34
+ File.dirname(config_file_directory)
35
+ end
36
+
37
+ end
@@ -0,0 +1,76 @@
1
+ require "droplet_kit"
2
+
3
+
4
+ class Provisional::Server
5
+
6
+ def self.list(environment)
7
+ all.map(&:name)
8
+ end
9
+
10
+ def self.all(environment = nil)
11
+ # TODO: Filter by environment (and domain), if given.
12
+ Provisional.digital_ocean.droplets.all.to_a
13
+ end
14
+
15
+ def self.create(options = {})
16
+ raise "Must pass in a hash" unless options.is_a?(Hash)
17
+ name = options[:name]
18
+ image = options[:image]
19
+ server_options = { region: 'nyc3', size: '512mb', ssh_keys: all_ssh_keys }
20
+ droplet = DropletKit::Droplet.new({name: name, image: image.id}.merge(server_options))
21
+ server = Provisional.digital_ocean.droplets.create(droplet)
22
+ print "Building server '#{name}' from image '#{image.slug || image.name}'."
23
+ $stdout.flush
24
+ while find(id: server.id).status == "new"
25
+ putc(".")
26
+ $stdout.flush
27
+ sleep 5
28
+ end
29
+ puts "DONE"
30
+ # Have to re-get the server, so it's populated with its IP address.
31
+ find(id: server.id)
32
+ end
33
+
34
+ def self.find(options = {})
35
+ raise "Must pass in a hash" unless options.is_a?(Hash)
36
+ if options[:id]
37
+ Provisional.digital_ocean.droplets.find(id: options[:id])
38
+ elsif options[:name]
39
+ all.select{|server| server.name == options[:name]}.first
40
+ end
41
+ end
42
+
43
+ def self.stop(server)
44
+ return unless server
45
+ action = Provisional.digital_ocean.droplet_actions.shutdown(droplet_id: server.id)
46
+ action_id = action.id
47
+ print "Stopping server '#{server.name}' (action_id = #{action_id})."
48
+ until action.status == "completed" do
49
+ putc(".")
50
+ # $stdout.flush
51
+ sleep 1
52
+ action = Provisional.digital_ocean.actions.find(id: action_id)
53
+ end
54
+ # TODO: I've seen a shutdown not work, so we'll need a timeout.
55
+ until Provisional.digital_ocean.droplets.find(id: server.id).status == "off" do
56
+ putc(".")
57
+ $stdout.flush
58
+ sleep 1
59
+ end
60
+ puts "DONE"
61
+ end
62
+
63
+
64
+ def self.delete(server)
65
+ return unless server
66
+ Provisional.digital_ocean.droplets.delete(id: server.id)
67
+ # Don't really need to wait for this operation to complete.
68
+ end
69
+
70
+ private
71
+
72
+ def self.all_ssh_keys
73
+ Provisional.digital_ocean.ssh_keys.all.to_a.map(&:id)
74
+ end
75
+
76
+ end
@@ -0,0 +1,3 @@
1
+ module Provisional
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'provisional/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "provisional"
7
+ spec.version = Provisional::VERSION
8
+ spec.authors = ["Craig Buchek"]
9
+ spec.email = ["craig@boochtek.com"]
10
+
11
+ spec.summary = %q{Manage and provision server images}
12
+ spec.description = %q{Provisional is a tool to manage and provision server images, currently on Digital Ocean}
13
+ spec.homepage = "https://github.com/boochtek/provisional"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "gli", "~> 2.13"
22
+ spec.add_dependency "droplet_kit", "~> 1.2"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.10"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "pry", "~> 0.10.1"
27
+ spec.add_development_dependency "cucumber", "~> 2.0"
28
+ spec.add_development_dependency "rspec", "~> 3.3"
29
+ spec.add_development_dependency "aruba", "~> 0.8.1"
30
+ end
metadata ADDED
@@ -0,0 +1,183 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: provisional
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Craig Buchek
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-08-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gli
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.13'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: droplet_kit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.10.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.10.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: cucumber
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: aruba
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.8.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.8.1
125
+ description: Provisional is a tool to manage and provision server images, currently
126
+ on Digital Ocean
127
+ email:
128
+ - craig@boochtek.com
129
+ executables:
130
+ - provisional
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - ".gitignore"
135
+ - CODE_OF_CONDUCT.md
136
+ - Gemfile
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - bin/console
141
+ - bin/setup
142
+ - data/default_config/infrastructure/base/scripts/00_update_packages.sh
143
+ - data/default_config/infrastructure/base/scripts/02_remove_packages.sh
144
+ - data/default_config/infrastructure/base/scripts/misc.sh
145
+ - data/default_config/infrastructure/base/scripts/ntp.sh
146
+ - data/default_config/infrastructure/base/scripts/sudo.sh
147
+ - data/default_config/infrastructure/db/scripts/postgresql
148
+ - data/default_config/infrastructure/lb/scripts/10_haproxy
149
+ - data/default_config/infrastructure/provisional.yml
150
+ - exe/provisional
151
+ - lib/provisional.rb
152
+ - lib/provisional/deployment.rb
153
+ - lib/provisional/image.rb
154
+ - lib/provisional/image_operations.rb
155
+ - lib/provisional/init.rb
156
+ - lib/provisional/server.rb
157
+ - lib/provisional/version.rb
158
+ - provisional.gemspec
159
+ homepage: https://github.com/boochtek/provisional
160
+ licenses:
161
+ - MIT
162
+ metadata: {}
163
+ post_install_message:
164
+ rdoc_options: []
165
+ require_paths:
166
+ - lib
167
+ required_ruby_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ required_rubygems_version: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ requirements: []
178
+ rubyforge_project:
179
+ rubygems_version: 2.4.5.1
180
+ signing_key:
181
+ specification_version: 4
182
+ summary: Manage and provision server images
183
+ test_files: []