arnold 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2013 Puppet Labs, info@puppetlabs.com
2
+ Copyright (c) 2013 FBL Financial, puppet@fblfinancial.com
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,92 @@
1
+ Introduction
2
+ ============
3
+
4
+ ### Arnold: *the provisionator*
5
+
6
+ This is the start of a Razor provisioning system.
7
+
8
+ Currently, all it does is provide a web service that will read & write YAML files
9
+ and a few functions to read them and apply classes to a node.
10
+
11
+ Very soon, I plan to provide Razor functionality to spin up new instances after
12
+ classifying them, and then it will be a simplistic self service provisioner.
13
+
14
+ Configuration
15
+ =============
16
+
17
+ * Installing
18
+ * It's a Puppet module, yo! Copy the `arnold` subdirectory to your modulepath.
19
+ * Alternately, you can install by hand by running the little installer script.
20
+ * Setup the server
21
+ 1. Classify your server with `arnold::provisionator` and apply.
22
+ 2. Configure by editing `/etc/arnold/config.yaml`
23
+ * You will probably want to point the `datadir` to wherever you've configured Hiera to use.
24
+ * You may configure Arnold to reuse Puppet certs if you wish.
25
+ * If you choose to generate your own SSL certs, drop them in /etc/arnold/certs
26
+ * `openssl genrsa -out server.key 1024`
27
+ * `openssl req -new -key server.key -out server.csr`
28
+ * `openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt`
29
+ 3. Point a web browser at the configured port
30
+ * Clicky clicky
31
+ * List nodes, create nodes, modify nodes, etc
32
+ 4. Enable arnold for clients:
33
+ * In `site.pp` on your master, outside of any node definitions, simply call the `arnold()` function.
34
+ * Alternately, you can classify your nodes with `arnold`
35
+ * If you place this class in your default group, it will automatically be applied to all nodes.
36
+
37
+ Provisioner backend configuration
38
+ =============
39
+
40
+ Currently only the CloudProvisioner provisioning backend exits. You can enable it by
41
+ adding a stanza like this to your `config.yaml`
42
+
43
+ backend: CloudProvisioner
44
+ keyfile: ~/.ssh/private_key.pem
45
+ enc_password: <password>
46
+
47
+ If a backend is not configured, Arnold will not perform any provisioning actions.
48
+ It is entirely useful like this, as it will instead only manage the Hiera datafiles.
49
+
50
+ Limitations
51
+ ============
52
+
53
+ * It does not currently manage parameterized classes.
54
+ * Razor support is not yet included.
55
+
56
+ Contact
57
+ =======
58
+
59
+ * Author: Ben Ford
60
+ * Email: ben.ford@puppetlabs.com
61
+ * Twitter: @binford2k
62
+ * IRC (Freenode): binford2k
63
+
64
+ Credit
65
+ =======
66
+
67
+ The development of this code was sponsored by FBL Financial.
68
+
69
+ License
70
+ =======
71
+
72
+ Copyright (c) 2013 Puppet Labs, info@puppetlabs.com
73
+ Copyright (c) 2013 FBL Financial, puppet@fblfinancial.com
74
+
75
+ Permission is hereby granted, free of charge, to any person obtaining
76
+ a copy of this software and associated documentation files (the
77
+ "Software"), to deal in the Software without restriction, including
78
+ without limitation the rights to use, copy, modify, merge, publish,
79
+ distribute, sublicense, and/or sell copies of the Software, and to
80
+ permit persons to whom the Software is furnished to do so, subject to
81
+ the following conditions:
82
+
83
+ The above copyright notice and this permission notice shall be
84
+ included in all copies or substantial portions of the Software.
85
+
86
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
87
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
88
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
89
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
90
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
91
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
92
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,82 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+ require 'optparse'
6
+ require 'pathname'
7
+
8
+ require 'arnold/node'
9
+ require 'arnold/node_manager'
10
+ require 'arnold/monkeypatch'
11
+ require 'arnold/controller'
12
+
13
+ cmdlineopts = {}
14
+ optparse = OptionParser.new { |opts|
15
+ opts.banner = "Usage : arnold [-c <confdir>] [-d]
16
+
17
+ Runs the arnold daemon.
18
+
19
+ "
20
+
21
+ opts.on("-b", "--backend BACKEND", "Choose an alternate provisioning backend.") do |opt|
22
+ cmdlineopts[:backend] = opt
23
+ end
24
+
25
+ opts.on("-c CONFIG", "--config CONFIG", "Choose an alternate config file. Defaults to /etc/arnold/config.yaml") do |opt|
26
+ configfile = opt
27
+ end
28
+
29
+ opts.on("-d", "--debug", "Run in the foreground and display debugging messages") do
30
+ # Separate options so daemonize = false doesn't force debug
31
+ cmdlineopts[:debug] = true
32
+ cmdlineopts[:daemonize] = false
33
+ end
34
+
35
+ opts.separator('')
36
+
37
+ opts.on("-h", "--help", "Displays this help") do
38
+ puts opts
39
+ exit
40
+ end
41
+ }
42
+ optparse.parse!
43
+
44
+ # Load default configuration data, deferring to command line overrides
45
+ configfile ||= '/etc/arnold/config.yaml'
46
+
47
+ begin
48
+ $CONFIG = YAML.load_file(configfile)
49
+ $CONFIG.merge!(cmdlineopts)
50
+ rescue Errno::ENOENT => e
51
+ puts "Config file doesn't exist; loading defaults! (#{e})" if cmdlineopts[:debug]
52
+ $CONFIG={}
53
+ end
54
+
55
+ $CONFIG[:docroot] ||= File.dirname(__FILE__) + '/../' # well isn't this ugly
56
+ $CONFIG[:backend] ||= 'Null'
57
+ $CONFIG[:datadir] ||= '/etc/arnold/data'
58
+ $CONFIG[:sslcert] ||= '/etc/arnold/certs/server.crt'
59
+ $CONFIG[:sslkey] ||= '/etc/arnold/certs/server.key'
60
+ $CONFIG[:port] ||= 9090
61
+ $CONFIG[:enc_server] ||= 'localhost'
62
+ $CONFIG[:enc_port] ||= 443
63
+ $CONFIG[:enc_user] ||= 'console'
64
+
65
+ begin
66
+ # load up our provisioning backend
67
+ require "arnold/provisioner/#{$CONFIG[:backend].to_underscore}"
68
+ $CONFIG[:provisioner] = Arnold::Provisioner::const_get($CONFIG[:backend]).new
69
+ rescue LoadError, NameError => e
70
+ puts "No such provisioning backend, loading null backend! (#{e})"
71
+ require 'arnold/provisioner/null'
72
+ $CONFIG[:provisioner] = Arnold::Provisioner::Null.new
73
+ end
74
+
75
+ case ARGV[0]
76
+ when 'serve'
77
+ Arnold::Controller::Web.new
78
+
79
+ else
80
+ Arnold::Controller::Cli.new(ARGV)
81
+
82
+ end
@@ -0,0 +1 @@
1
+ # Place certificates in this directory
@@ -0,0 +1,15 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIICRzCCAbACCQDZGL+AiHPpyjANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJV
3
+ UzEPMA0GA1UECBMGT3JlZ29uMREwDwYDVQQHEwhQb3J0bGFuZDEUMBIGA1UEChML
4
+ UHVwcGV0IExhYnMxCzAJBgNVBAsTAlBTMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcN
5
+ MTIwOTE5MTcxMTQyWhcNMTMwOTE5MTcxMTQyWjBoMQswCQYDVQQGEwJVUzEPMA0G
6
+ A1UECBMGT3JlZ29uMREwDwYDVQQHEwhQb3J0bGFuZDEUMBIGA1UEChMLUHVwcGV0
7
+ IExhYnMxCzAJBgNVBAsTAlBTMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZI
8
+ hvcNAQEBBQADgY0AMIGJAoGBAKjECcBJqQx5G479gtFVSF7M9noXEcTG6tV/Z3SW
9
+ h/N5PjZzovcc9tKzCRfeCzVGupkUblkxwhjP+gWSjcPoioIvG1twfQIDuiG6wtH5
10
+ FfS2q8creJZwd9OxGMTb4mkmfaY+88N9vt0a+OyLE6L9Cw6+CoB2XNAwgcNWKdzs
11
+ QzfPAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAbItcPsOqCqktv+TuXrCgiYdmQjEl
12
+ R/xKqDUGSNkkUJCEjmff6VJslPzdteIkv42trJZMbbQY2SR1eylt7/AmLXmSqEJh
13
+ ivnAF3MCAg59RF3P5RoqFmjUJZcsoGZp7v6d5y2/HB712JKmiohIWj82H6iayW+k
14
+ 43jfo4DvdfNUL7I=
15
+ -----END CERTIFICATE-----
@@ -0,0 +1,15 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIICXQIBAAKBgQCoxAnASakMeRuO/YLRVUhezPZ6FxHExurVf2d0lofzeT42c6L3
3
+ HPbSswkX3gs1RrqZFG5ZMcIYz/oFko3D6IqCLxtbcH0CA7ohusLR+RX0tqvHK3iW
4
+ cHfTsRjE2+JpJn2mPvPDfb7dGvjsixOi/QsOvgqAdlzQMIHDVinc7EM3zwIDAQAB
5
+ AoGAWsFJaSFziiSagFOuBLpy96ALL+61/HboFDW2QckthO3/WbLnwTHPPdFPo4kh
6
+ x92oPOfyy35pnYRCNLryB5dG3Al9b6x9G5wx5PqXASShFhgjrUYTA0bPnAeMM7po
7
+ b8x5rMssVhCPYYxmrKVyTEsmzL8EhGcEArEyFfL4JSaEA0ECQQDaMies6EeTeQ3v
8
+ v2hJUiknC4T++RfAubo1/mk4y4U7WUbmLGUXOE6txCbqRMs7HUZrL/kDmxORWudo
9
+ XSevQSPvAkEAxgF2WFMR0C1PpMbKaE8CSQfAOawt9u3eJ+TSRBHa3k+IRb8dg2OQ
10
+ yfbEc1YvUBE3Ycz8eOu5WBzoFLf7uq3KIQJAToNJn4AdcUVX7HL1dZyozjHo805y
11
+ a5jpFlCrUBJ7qHVhe6Vx4r8SIJi6YAXNE0JfemZStidxDRamufj7NKa95QJBALIG
12
+ po0LQzzVQIJ6aYoXX4qh+WbhNAKMI+3iglrJYuv2viNXjgWQA6JSyJaaqrdmg1Df
13
+ qTBfYKmkc9YNBbv2fYECQQDMSMx9gkDu/Rhv8fF8y4gUrVqSh8/aSWqrbaEQwEkp
14
+ MPbCuYg/dh0Epb+btmed5+b3zcQImlPccP0JZGs0nFa4
15
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,16 @@
1
+ ---
2
+ :sslcert: /etc/arnold/certs/server.crt
3
+ :sslkey: /etc/arnold/certs/server.key
4
+ :datadir: /etc/arnold/data
5
+
6
+ :user: admin
7
+ :password: admin
8
+
9
+ :daemonize: false
10
+ :port: 9090
11
+
12
+ :classes:
13
+ apache: Manage the Apache webserver
14
+ mysql: Manage the MySQL database
15
+ ntp::server: Install the NTP server
16
+ ntp::client: Install the NTP client and configure the node to query the internal NTP server
@@ -0,0 +1,42 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # A quick 'n dirty example of a simple script to exercise Arnold's REST API
4
+
5
+ require 'net/https'
6
+ require 'rubygems'
7
+ require 'json'
8
+ require 'uri'
9
+
10
+ user = 'admin'
11
+ pass = 'admin'
12
+ server = 'localhost'
13
+
14
+ def provision()
15
+ # Call VMware APIs to create a new machine, configure, and boot it.
16
+ # Return its MAC address.
17
+ return '00:0C:29:D1:03:A4'
18
+ end
19
+
20
+ macaddr = provision()
21
+
22
+ payload = {
23
+ 'macaddr' => macaddr,
24
+ 'name' => 'this.is.another.brand.new.system',
25
+ 'parameters' => {
26
+ 'booga' => 'wooga',
27
+ 'fiddle' => 'faddle',
28
+ },
29
+ 'classes' => [ 'test', 'mysql', 'ntp' ],
30
+ }.to_json
31
+
32
+ uri = URI.parse("https://#{server}:9090/api/v1/create")
33
+ http = Net::HTTP.new(uri.host, uri.port)
34
+ http.use_ssl = true
35
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
36
+ request = Net::HTTP::Post.new(uri.request_uri)
37
+ request.add_field('Content-Type', 'application/json')
38
+ request.basic_auth(user, pass)
39
+ request.body = payload
40
+ response = http.request(request)
41
+ puts "Response #{response.code} #{response.message}: #{response.body}"
42
+
@@ -0,0 +1,6 @@
1
+ module Arnold
2
+ module Controller
3
+ autoload :Web, 'arnold/controller/web'
4
+ autoload :Cli, 'arnold/controller/cli'
5
+ end
6
+ end
@@ -0,0 +1,65 @@
1
+ require 'arnold/node'
2
+ require 'arnold/node_manager'
3
+
4
+ module Arnold
5
+ module Controller
6
+ class Cli
7
+
8
+ def initialize(args)
9
+ @manager = Arnold::NodeManager.new
10
+
11
+ case args[0]
12
+ when "help"
13
+ usage
14
+ when "list"
15
+ listnodes
16
+ exit 0
17
+ when "new"
18
+ args.shift
19
+ @data = {}
20
+ args.each do |arg|
21
+ name, value = arg.split("=")
22
+ @data[name] = value
23
+ end
24
+
25
+ begin
26
+ node = Arnold::Node.new(nil,
27
+ @data['name'],
28
+ @data['macaddr'],
29
+ Arnold::Node.munge(@data, :params),
30
+ @data['classes'].split(','))
31
+ @manager.write(node)
32
+
33
+ $CONFIG[:provisioner].provision(node)
34
+ rescue RuntimeError => e
35
+ puts "Whoops: #{e}"
36
+ end
37
+ else
38
+ puts "WAT"
39
+ usage
40
+ end
41
+ end
42
+
43
+ def listnodes
44
+ nodes = @manager.loadall
45
+ puts
46
+ puts "________GUID______________________Name____________________MAC Address___"
47
+ nodes.each do |node|
48
+ printf "%18s │ %30s │ %18s\n", node.guid, node.name, node.macaddr
49
+ end
50
+ puts
51
+ end
52
+
53
+ def usage
54
+ puts
55
+ puts "Usage:"
56
+ puts " * arnold help"
57
+ puts " * arnold list"
58
+ puts " * arnold new [name=<name>] [macaddr=<macaddr>] [template=<template>] [group=<group>] [classes=<class1,class2,...>] [param1=value1]..."
59
+ puts
60
+ exit 1
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,31 @@
1
+ require 'sinatra/base'
2
+ require 'webrick'
3
+ require 'webrick/https'
4
+ require 'openssl'
5
+
6
+ require 'arnold/server'
7
+
8
+ module Arnold
9
+ module Controller
10
+ class Web
11
+ def initialize
12
+ opts = {
13
+ :Port => $CONFIG[:port] || 8080,
14
+ :Logger => WEBrick::Log::new($stderr, WEBrick::Log::DEBUG),
15
+ :ServerType => $CONFIG[:daemonize] ? WEBrick::Daemon : WEBrick::SimpleServer,
16
+ :DocumentRoot => $CONFIG[:docroot],
17
+ :SSLEnable => true,
18
+ :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE,
19
+ :SSLCertificate => OpenSSL::X509::Certificate.new( File.open($CONFIG[:sslcert]).read),
20
+ :SSLPrivateKey => OpenSSL::PKey::RSA.new( File.open($CONFIG[:sslkey]).read),
21
+ :SSLCertName => [ [ "CN",WEBrick::Utils::getservername ] ]
22
+ }
23
+
24
+ # now it's off to the races!
25
+ Rack::Handler::WEBrick.run(Arnold::Server, opts) do |server|
26
+ [:INT, :TERM].each { |sig| trap(sig) { server.stop } }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ if not String.method_defined? :to_underscore
2
+ class String
3
+ # ruby mutation methods have the expectation to return self if a mutation
4
+ # occurred, nil otherwise.
5
+ # (see http://www.ruby-doc.org/core-1.9.3/String.html#method-i-gsub-21)
6
+ def to_underscore!
7
+ gsub!(/(.)([A-Z])/,'\1_\2') && downcase!
8
+ end
9
+
10
+ def to_underscore
11
+ dup.tap { |s| s.to_underscore! }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,86 @@
1
+ module Arnold
2
+ class Node
3
+ attr_reader :guid, :name, :macaddr, :parameters, :classes
4
+ @@reserved_params = ['guid', 'name', 'macaddr', 'classes']
5
+
6
+ def initialize(guid=nil, name=nil, macaddr=nil, parameters={}, classes = [])
7
+ # if no guid, this is intended to be a new node
8
+ self.guid = guid
9
+ self.name = name
10
+ self.macaddr = macaddr
11
+ self.parameters = parameters
12
+ self.classes = classes
13
+ end
14
+
15
+ def guid=(g)
16
+ @guid = validate(g, :filename)
17
+ end
18
+
19
+ def name=(n)
20
+ @name = validate(n, :filename)
21
+ end
22
+
23
+ def macaddr=(m)
24
+ @macaddr = validate(m, :macaddr)
25
+ end
26
+
27
+ def parameters=(p)
28
+ @parameters = validate(p, :params)
29
+ end
30
+
31
+ def classes=(c)
32
+ @classes = validate(c, :classes)
33
+ end
34
+
35
+ # returns classes and descriptions for classes enabled or disabled
36
+ def enabled
37
+ return {} if @classes.nil?
38
+ $CONFIG[:classes].select { |name, desc| @classes.include? name }
39
+ end
40
+
41
+ def disabled
42
+ return $CONFIG[:classes] if @classes.nil?
43
+ $CONFIG[:classes].reject { |name, desc| @classes.include? name }
44
+ end
45
+
46
+ # Raise exceptions if the given condition fails
47
+ #
48
+ def validate(value, type=:exists)
49
+ case type
50
+ when :classes
51
+ return if value.nil?
52
+ raise "Invalid type: #{value.class}" if not value.kind_of?(Array)
53
+ value.each { |n| raise "Invalid class: #{n}" if not $CONFIG[:classes].has_key?(n) }
54
+
55
+ when :params
56
+ return if value.nil?
57
+ raise "Invalid type: #{value.class}" if not value.kind_of?(Hash)
58
+ @@reserved_params.each { |n| raise "Invalid parameter: #{n}" if value.has_key?(n) }
59
+
60
+ when :macaddr
61
+ return if value.nil?
62
+ value.upcase!
63
+ raise "Invalid MAC address: #{value}" if not value =~ /^(([0-9A-F]{2}[:-]){5}([0-9A-F]{2}))?$/
64
+
65
+ when :filename
66
+ return if value.nil?
67
+ raise "Invalid name: #{value}" if not value =~ /^([^\/])*$/
68
+
69
+ when :exists
70
+ raise "Value does not exist." if (value.nil? || value.empty?)
71
+
72
+ end
73
+
74
+ return value
75
+ end
76
+
77
+ def self.munge(value, type=:upcase)
78
+ case type
79
+ when :upcase
80
+ return value.nil? ? nil : value.upcase
81
+ when :params
82
+ return value.reject { |name, val| @@reserved_params.include? name }
83
+ end
84
+ end
85
+ end
86
+ end