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 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!
@@ -0,0 +1,4 @@
1
+ class Dockershell::Wordgen
2
+
3
+
4
+ end
@@ -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