dockershell 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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