dockershell 0.0.1
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.
- 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
|