dockershell 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +164 -0
- data/bin/dockershell +65 -0
- data/lib/dockershell/wordgen.rb +4 -0
- data/lib/dockershell.rb +133 -0
- data/resources/places.txt +3101 -0
- data/scripts/course_selector +29 -0
- data/scripts/pe_classify +39 -0
- data/scripts/pe_purge +37 -0
- metadata +69 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dc4ea96dd7610b476d4fc51ac64945bd935a0c66
|
4
|
+
data.tar.gz: efd2a909162434eaa8446c0bbf7f2f2f578254dd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 646561ae7ef2869024cbe57e1a2beb868f8e902e5863352fc59c7c29e9cd81d88dc465e32c08a70c66344bf5d8afdc8569fd23ff475773c9a405ad34808f4a29
|
7
|
+
data.tar.gz: 076319a5aaa3af6fe0d01b120904198dfe2c29f04edfd5040d365265d4205f0371a9bced9b5f95030e33c55978493b9358c7c514a408eabe8024337ad8225c99
|
data/README.md
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
# dockershell
|
2
|
+
A simple user shell backed by Docker containers.
|
3
|
+
|
4
|
+
#### Table of Contents
|
5
|
+
|
6
|
+
1. [Overview](#overview)
|
7
|
+
1. [Configuration](#configuration)
|
8
|
+
* [Profiles](#profiles)
|
9
|
+
1. [Usage Suggestions](#usage-suggestions)
|
10
|
+
1. [Limitations](#limitations)
|
11
|
+
|
12
|
+
## Overview
|
13
|
+
|
14
|
+
Dockershell can be used as a user login shell, or simply run on the CLI. It
|
15
|
+
will stand up a Docker container and then drop the user into a bash shell
|
16
|
+
on the container. It's highly configurable and supports multiple profiles.
|
17
|
+
|
18
|
+
Note: When combined with my [Abalone](https://github.com/binford2k/abalone) web
|
19
|
+
terminal project, this allows you to expose Docker-backed shells to a web browser.
|
20
|
+
See [usage suggestions](#usage-suggestions) below.
|
21
|
+
|
22
|
+
## Configuration
|
23
|
+
|
24
|
+
The configuration is always loaded from `/etc/dockershell/config.yaml`. For security
|
25
|
+
purposes, this location is not relocatable or configurable. Sensible defaults are
|
26
|
+
provided, so configuration is not strictly required.
|
27
|
+
|
28
|
+
* `:loglevel`
|
29
|
+
* Standard log levels: `:fatal`, `:error`, `:warn`, `:info`, `:debug`
|
30
|
+
* Default value: `:warn`
|
31
|
+
* `:logfile`
|
32
|
+
* Where you want to log to.
|
33
|
+
* Default value: `/var/log/dockershell`
|
34
|
+
* `:domain`
|
35
|
+
* The domain to use for container FQDNs
|
36
|
+
* Default value: the domain of the host.
|
37
|
+
* `:docker`
|
38
|
+
* A `Hash` of Docker settings.
|
39
|
+
* `:group`
|
40
|
+
* The group allowed to access Docker containers.
|
41
|
+
* Default value: `docker`
|
42
|
+
* `:ipaddr`
|
43
|
+
* The address of the Docker network interface.
|
44
|
+
* Default value: the address of interface `docker0`
|
45
|
+
|
46
|
+
#### Example:
|
47
|
+
|
48
|
+
``` yaml
|
49
|
+
---
|
50
|
+
:loglevel: :warn
|
51
|
+
:logfile: /var/log/dockershell
|
52
|
+
:domain: try.puppet.com
|
53
|
+
:docker:
|
54
|
+
:group: docker
|
55
|
+
:ipaddr: 1.2.3.4
|
56
|
+
```
|
57
|
+
|
58
|
+
### Profiles
|
59
|
+
|
60
|
+
Profiles are how you configure different ways to run the shell. Without a profile,
|
61
|
+
Dockershell will simply log you into a new container and destroy it when you log
|
62
|
+
out. By defining a profile, you can specify the container name and a series of
|
63
|
+
scripts that you can use to configure the container.
|
64
|
+
|
65
|
+
Each key of the `:profiles` hash should be the name of a profile. In the following
|
66
|
+
example, two profiles (`learn` and `docs`) are defined:
|
67
|
+
|
68
|
+
|
69
|
+
``` Yaml
|
70
|
+
---
|
71
|
+
:profiles:
|
72
|
+
:learn:
|
73
|
+
:image: agent
|
74
|
+
:prerun: pe_classify
|
75
|
+
:setup: course_selector
|
76
|
+
:postrun: pe_purge
|
77
|
+
:docs:
|
78
|
+
:image: agent
|
79
|
+
```
|
80
|
+
|
81
|
+
Each profile has a number of options you can configure. None have default values.
|
82
|
+
|
83
|
+
* `:image`
|
84
|
+
* The name of the Docker image to run.
|
85
|
+
* Required.
|
86
|
+
* `:prerun`
|
87
|
+
* The name of a script to run before the container is created.
|
88
|
+
* `:setup`
|
89
|
+
* A script to run after the container is started, but before the user is logged in.
|
90
|
+
* `:postrun`
|
91
|
+
* The cleanup script to run after the session has ended. It is run in a detached
|
92
|
+
process so that it's guaranteed to complete even if the shell is killed.
|
93
|
+
* *This will not execute if the shell is terminated with signal 9 (`SIGKILL`)!*
|
94
|
+
|
95
|
+
Each script should exist in `/etc/dockershell/scripts`. A few defaults are provided
|
96
|
+
directly in the gem. The script is executed with two values passed:
|
97
|
+
|
98
|
+
1. `ARGV[0]` is the FQDN of the container node.
|
99
|
+
* The first segment will be the name of the container.
|
100
|
+
1. `ARGV[1]` is the value of the optional `--option` parameter.
|
101
|
+
|
102
|
+
#### Built in scripts:
|
103
|
+
|
104
|
+
* `pe_classify` will pin the container to a new environment group in the PE Console.
|
105
|
+
* An environment directory is created on disk if needed.
|
106
|
+
* `pe_purge` will remove the container from the Puppet Enterprise infrastructure.
|
107
|
+
* The environment directory is removed.
|
108
|
+
* The environment node group is removed.
|
109
|
+
* The node's certificate is cleaned.
|
110
|
+
* The node is purged from PuppetDB.
|
111
|
+
* `course_selector` will classify the new node with the name of a Puppet Education course.
|
112
|
+
* Pass the course name to Dockershell with `--option`
|
113
|
+
|
114
|
+
|
115
|
+
## Usage Suggestions
|
116
|
+
|
117
|
+
When combined with my [Abalone](https://github.com/binford2k/abalone) web
|
118
|
+
terminal project, this allows you to expose Docker-backed shells to a web
|
119
|
+
browser. The Abalone configuration could look like:
|
120
|
+
|
121
|
+
``` yaml
|
122
|
+
---
|
123
|
+
:port: 9000
|
124
|
+
:bind: 0.0.0.0
|
125
|
+
:logfile: /var/log/abalone
|
126
|
+
:bannerfile: /etc/dockershell/banner
|
127
|
+
:command: /usr/local/bin/dockershell
|
128
|
+
:timeout: 900
|
129
|
+
:ttl: 60
|
130
|
+
:params: ['profile', 'option']
|
131
|
+
```
|
132
|
+
|
133
|
+
If you prefer to be more prescriptive, you can specify allowed values for
|
134
|
+
parameters, such as:
|
135
|
+
|
136
|
+
``` yaml
|
137
|
+
:params:
|
138
|
+
profile:
|
139
|
+
:values: ['learn', 'docs']
|
140
|
+
option:
|
141
|
+
:values:
|
142
|
+
- default
|
143
|
+
- hiera
|
144
|
+
- parser
|
145
|
+
- testing
|
146
|
+
```
|
147
|
+
|
148
|
+
Then you would simply load the web terminal with a URL such as:
|
149
|
+
http://login.example.com:9000/?profile=learn&option=parser
|
150
|
+
|
151
|
+
|
152
|
+
## Limitations
|
153
|
+
|
154
|
+
This is super early in development and has not yet been battle tested.
|
155
|
+
|
156
|
+
|
157
|
+
## Disclaimer
|
158
|
+
|
159
|
+
I take no liability for the use of this tool.
|
160
|
+
|
161
|
+
Contact
|
162
|
+
-------
|
163
|
+
|
164
|
+
binford2k@gmail.com
|
data/bin/dockershell
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'optparse'
|
5
|
+
require 'yaml'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
require 'facter'
|
9
|
+
|
10
|
+
require 'dockershell'
|
11
|
+
|
12
|
+
config = '/etc/dockershell/config.yaml'
|
13
|
+
options = {
|
14
|
+
:loglevel => 'info',
|
15
|
+
:logfile => '/var/log/dockershell',
|
16
|
+
:domain => Facter.value(:domain),
|
17
|
+
:docker => {
|
18
|
+
:group => 'docker',
|
19
|
+
:ipaddr => Facter.value(:ipaddress_docker0),
|
20
|
+
},
|
21
|
+
:profile => {
|
22
|
+
:image => 'agent',
|
23
|
+
}
|
24
|
+
}
|
25
|
+
options.merge!(YAML.load_file(config)) if File.file? config
|
26
|
+
options[:logger] = Logger.new(options[:logfile])
|
27
|
+
options[:logger].level = Logger::const_get(options[:loglevel].upcase)
|
28
|
+
|
29
|
+
optparse = OptionParser.new { |opts|
|
30
|
+
opts.banner = "Usage : dockershell [-p <profile>] [-n <name>] [-d]
|
31
|
+
-- Starts a shell by running a Docker container.
|
32
|
+
|
33
|
+
"
|
34
|
+
|
35
|
+
opts.on("-d", "--debug", "Display or log debugging messages") do
|
36
|
+
options[:logger].level = Logger::DEBUG
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("-n NAME", "--name NAME", "Provide a name for your container.") do |arg|
|
40
|
+
options[:name] = arg
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("-p PROFILE", "--profile PROFILE", "Which profile to start. Defaults to 'default'.") do |arg|
|
44
|
+
next unless options.include? :profiles
|
45
|
+
next unless options[:profiles].include? arg.to_sym
|
46
|
+
options[:profile] = options[:profiles][arg.to_sym]
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on("-o OPTION", "--option OPTION", "Pass a custom option for prerun/setup/postrun tasks.") do |arg|
|
50
|
+
options[:option] = arg
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.separator('')
|
54
|
+
|
55
|
+
opts.on("-h", "--help", "Displays this help") do
|
56
|
+
puts
|
57
|
+
puts opts
|
58
|
+
puts
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
}
|
62
|
+
optparse.parse!
|
63
|
+
options.delete :profiles
|
64
|
+
|
65
|
+
Dockershell.new(options).run!
|
data/lib/dockershell.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
class Dockershell
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
@logger = options[:logger]
|
8
|
+
@gempath = File.expand_path('..', File.dirname(__FILE__))
|
9
|
+
|
10
|
+
@options[:name] ||= wordgen
|
11
|
+
@options[:fqdn] ||= "#{@options[:name]}.#{@options[:domain]}"
|
12
|
+
@options[:profile][:volumes] ||= []
|
13
|
+
|
14
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
15
|
+
"#{datetime} #{severity.ljust(5)} [#{@options[:name]}] #{msg}\n"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def run!
|
20
|
+
at_exit do
|
21
|
+
detached_postrun
|
22
|
+
end
|
23
|
+
prerun
|
24
|
+
create unless running?
|
25
|
+
setup
|
26
|
+
start
|
27
|
+
end
|
28
|
+
|
29
|
+
def start
|
30
|
+
@logger.info 'starting Dockershell.'
|
31
|
+
args = 'docker', 'exec', '-it', @options[:name], 'script', '-qc', 'bash', '/dev/null'
|
32
|
+
bomb 'could not start container.' unless system(*args)
|
33
|
+
end
|
34
|
+
|
35
|
+
[:prerun, :setup, :postrun].each do |task|
|
36
|
+
define_method(task) do
|
37
|
+
return unless @options[:profile].include? task
|
38
|
+
script = which(@options[:profile][task]) || return
|
39
|
+
@logger.debug "#{task}: #{script} #{@options[:fqdn]} #{@options[:option]}"
|
40
|
+
|
41
|
+
# This construct allows us to have an optional 2nd parameter to the script
|
42
|
+
output, status = Open3.capture2e(*[script, @options[:fqdn], @options[:option]].compact)
|
43
|
+
@logger.debug output
|
44
|
+
bomb "#{task} task '#{script} #{@options[:name]} #{@options[:option]}' failed." unless status.success?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# This spawns a detached process to clean up. This is so it doesn't die when the parent is killed
|
49
|
+
def detached_postrun
|
50
|
+
@logger.info 'terminating and cleaning up.'
|
51
|
+
cleaner = Process.fork do
|
52
|
+
Process.setsid
|
53
|
+
|
54
|
+
output, status = Open3.capture2e('docker', 'kill', @options[:name])
|
55
|
+
@logger.debug output
|
56
|
+
@logger.warn 'could not stop container' unless status.success?
|
57
|
+
|
58
|
+
output, status = Open3.capture2e('docker', 'rm', '-f', @options[:name])
|
59
|
+
@logger.debug output
|
60
|
+
@logger.warn 'could not remove container' unless status.success?
|
61
|
+
|
62
|
+
postrun
|
63
|
+
end
|
64
|
+
Process.detach cleaner
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def running?
|
69
|
+
data = `docker ps -a`.split("\n")
|
70
|
+
data.shift # remove column header
|
71
|
+
|
72
|
+
names = data.map { |line| line.split.last }
|
73
|
+
if names.include? @options[:name]
|
74
|
+
output, status = Open3.capture2e('docker', 'inspect', @options[:name])
|
75
|
+
bomb 'could not get container info.' unless status.success?
|
76
|
+
|
77
|
+
info = JSON.parse(output)
|
78
|
+
bomb 'multiple containers with this name exist.' unless info.size == 1
|
79
|
+
|
80
|
+
info = info.first
|
81
|
+
@logger.debug(info['State'].inspect)
|
82
|
+
|
83
|
+
bomb 'Inconsistent Docker state.' unless info['State']['Running']
|
84
|
+
true
|
85
|
+
else
|
86
|
+
false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def create
|
91
|
+
@logger.info 'creating container.'
|
92
|
+
args = [
|
93
|
+
'docker', 'run',
|
94
|
+
'--security-opt', 'seccomp=unconfined',
|
95
|
+
'--stop-signal=SIGRTMIN+3',
|
96
|
+
'--tmpfs', '/tmp', '--tmpfs', '/run',
|
97
|
+
'--volume', '/sys/fs/cgroup:/sys/fs/cgroup:ro',
|
98
|
+
'--hostname', "#{@options[:fqdn]}",
|
99
|
+
'--name', @options[:name],
|
100
|
+
"--add-host=puppet:#{@options[:docker][:ipaddr]}",
|
101
|
+
'--expose=80', '-Ptd',
|
102
|
+
]
|
103
|
+
|
104
|
+
@options[:profile][:volumes].each do |volume|
|
105
|
+
args << '--volume' << volume
|
106
|
+
end
|
107
|
+
|
108
|
+
args << @options[:profile][:image] << '/sbin/init'
|
109
|
+
|
110
|
+
output, status = Open3.capture2e(*args)
|
111
|
+
@logger.debug output
|
112
|
+
bomb 'could not create container.' unless status.success?
|
113
|
+
end
|
114
|
+
|
115
|
+
def bomb(message)
|
116
|
+
@logger.warn message
|
117
|
+
raise "[#{@options[:name]}] #{message}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def which(name)
|
121
|
+
["/etc/dockershell/scripts/#{name}", "#{@gempath}/scripts/#{name}"].each do |path|
|
122
|
+
return path if File.file? path and File.executable? path
|
123
|
+
end
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def wordgen
|
128
|
+
words = File.readlines("#{@gempath}/resources/places.txt").each { |l| l.chomp! }
|
129
|
+
|
130
|
+
"#{words.sample}-#{words.sample}"
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|