provisional 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +86 -0
- data/Rakefile +1 -0
- data/bin/console +10 -0
- data/bin/setup +7 -0
- data/data/default_config/infrastructure/base/scripts/00_update_packages.sh +4 -0
- data/data/default_config/infrastructure/base/scripts/02_remove_packages.sh +5 -0
- data/data/default_config/infrastructure/base/scripts/misc.sh +7 -0
- data/data/default_config/infrastructure/base/scripts/ntp.sh +23 -0
- data/data/default_config/infrastructure/base/scripts/sudo.sh +63 -0
- data/data/default_config/infrastructure/db/scripts/postgresql +3 -0
- data/data/default_config/infrastructure/lb/scripts/10_haproxy +3 -0
- data/data/default_config/infrastructure/provisional.yml +45 -0
- data/exe/provisional +78 -0
- data/lib/provisional.rb +31 -0
- data/lib/provisional/deployment.rb +51 -0
- data/lib/provisional/image.rb +43 -0
- data/lib/provisional/image_operations.rb +103 -0
- data/lib/provisional/init.rb +37 -0
- data/lib/provisional/server.rb +76 -0
- data/lib/provisional/version.rb +3 -0
- data/provisional.gemspec +30 -0
- metadata +183 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -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,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
|
data/exe/provisional
ADDED
@@ -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)
|
data/lib/provisional.rb
ADDED
@@ -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
|
data/provisional.gemspec
ADDED
@@ -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: []
|