roxanne 0.1.0

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: c5d851cce33eec10554348917d888de2850376f4
4
+ data.tar.gz: 8170b18990fa9ad11befe6a0da395acdf487cda8
5
+ SHA512:
6
+ metadata.gz: ea7d837226ee30baa49c0efec2b78242803de3190d173abc2b2c1ffc4cf9db4321549dff008e6b9b56fb8f6ee5f10328f2424b73a49d08937ea4c4fa49e0ba86
7
+ data.tar.gz: cc624af0b25c3717e0a8c4a247d3c18110308e2700c1c2fa538f634311e0d6a39e851db05d19eb2dd2a270fd7c3ffc696c00b75656c352a9d956590390383f36
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ Gemfile.lock
2
+ roxanne.log
3
+ roxanne.pid
4
+ .*.swp
5
+ pkg
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.1
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.1
6
+ notifications:
7
+ hipchat:
8
+ rooms:
9
+ secure: B+SptkYk6J9+PPJnqzA8S4hce6veT/bHciATE5MEA6fBd4gZQ2YMACgdaPqqzv1TU2Lr725uKFRpMQkE/E5B6oLGBcck7tAzZccKKSLiXyKMxgpyEcw+WC2laGhqqnfzd83iYjQujMy6kwksTknmgLV34HIuMUcyREYovEIxPgA=
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in roxanne.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,7 @@
1
+ guard :minitest do
2
+ # with Minitest::Spec
3
+ watch(%r{^spec/(.*)_spec\.rb$})
4
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
5
+ watch(%r{^spec/spec_helper\.rb$}) { 'spec' }
6
+ watch(%r{^spec/support/(.*)\.rb$}) { 'spec' }
7
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jef Mathiot
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # Roxanne
2
+
3
+ Roxanne allows you to aggregate the status of Continuous Integration jobs or other sources and to
4
+ publish the results on a BigVisibleThing™ (traffic light, lava lamp, whatever).
5
+
6
+ [![Build Status](https://travis-ci.org/servebox/roxanne.png)](https://travis-ci.org/servebox/roxanne)
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'roxanne'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install roxanne
21
+
22
+ ## Usage
23
+
24
+ ### Configuration
25
+
26
+ Configuration is stored in a YAML file. By default, Roxanne will search for a `config/config.yml`
27
+ file.
28
+
29
+ #### Activation
30
+
31
+ Roxanne is configured by default to only be active between 9am and 20pm during working days. You
32
+ may use the `activation` section to change the default behavior:
33
+
34
+ ```yaml
35
+ roxanne:
36
+ activation:
37
+ # Active from monday to wednesday
38
+ days: !ruby/range 1..3
39
+ # 07:00 to 14:59
40
+ timerange: !ruby/range 7...15
41
+ ```
42
+
43
+ #### Consumers
44
+
45
+ Use the `consumers` section to specify consumers configuration:
46
+
47
+ ```yaml
48
+ roxanne:
49
+ consumers:
50
+ jenkins:
51
+ class: "Roxanne::Jenkins::Consumer"
52
+ host: "192.168.20.30"
53
+ port: 8080
54
+ use_ssl: false
55
+ path: "/api/json"
56
+ ```
57
+
58
+ #### Publisher
59
+
60
+ Use the `publisher` section to specify consumers configuration:
61
+
62
+ ```yaml
63
+ roxanne:
64
+ publisher:
65
+ class: "Roxanne::GPIO::Publisher"
66
+ green_pin: 25
67
+ orange_pin: 17
68
+ red_pin: 27
69
+ ```
70
+
71
+ ### Start the daemon
72
+
73
+ Start the daemon using the `start` command:
74
+
75
+ ```
76
+ bundle exec roxanne start
77
+ ```
78
+
79
+ If you want to specify a configuration file pass its path as the first argument:
80
+
81
+ ```
82
+ bundle exec roxanne start /etc/roxanne/config.yml
83
+ ```
84
+
85
+ Other available commands are obviously `stop`, `status` and `restart`.
86
+
87
+ ### Available Consumers
88
+
89
+ #### Jenkins
90
+
91
+ Use the `Roxanne::Jenkins::Consumer` class. Available options:
92
+
93
+ * **host**: the Jenkins host name or IP address
94
+ * **port**: the TCP port the Jenkins service can be reached on
95
+ * **path**: the relative path to the JSON API (`/jenkins/api/json`)
96
+ * **username**: the username to use for HTTP Basic Auth
97
+ * **password**: the password to use for HTTP Basic Auth
98
+ * **disable_certificate_verification** : set to true if you use a self-signed SSL certificate
99
+
100
+ #### Travis CI
101
+
102
+ Use the `Roxanne::Travis::Consumer` class. Available options:
103
+
104
+ * **organization_or_user**: the Github organization or user to pull build states from
105
+
106
+ ### Available Publishers
107
+
108
+ #### GPIO
109
+
110
+ Use the `Roxanne::GPIO::Consumer` class. Available options:
111
+
112
+ * **green_pin**: the GPIO pin to turn on when status changes to green
113
+ * **orange_pin**: the GPIO pin to turn on when status changes to orange
114
+ * **red_pin**: the GPIO pin to turn on when status changes to red
115
+
116
+ ## Statuses
117
+
118
+ Every 5 seconds, Roxanne will loop to check the status of the consumers:
119
+
120
+ * if any of the consumers returns `red` the publisher receive `red`
121
+ * if all of the consumers return `green` the publisher receive `green`
122
+ * if the previous state **was not** `green` and one of the consumer returns `orange` the publisher
123
+ receive `orange`
124
+
125
+ ## Contributing
126
+
127
+ 1. Fork it
128
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
129
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
130
+ 4. Push to the branch (`git push origin my-new-feature`)
131
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'spec'
6
+ t.pattern = "spec/**/*_spec.rb"
7
+ end
8
+
9
+ task :default => [:test]
data/bin/roxanne ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'roxanne'
4
+ require 'fileutils'
5
+
6
+ basedir = File.expand_path(File.join( File.dirname(__FILE__) , '..'))
7
+ %w(log pids).each do |dir|
8
+ dir = File.join(basedir, dir)
9
+ FileUtils.mkdir_p dir unless File.directory?(dir)
10
+ end
11
+ Roxanne::Server.spawn!(
12
+ log_file: File.join(basedir, 'log/roxanne.log'),
13
+ pid_file: File.join(basedir, 'pids/roxanne.pid'),
14
+ sync_log: true,
15
+ working_dir: basedir
16
+ )
data/config/config.yml ADDED
@@ -0,0 +1,6 @@
1
+ roxanne:
2
+ consumers:
3
+ test:
4
+ class: "Roxanne::Test::Consumer"
5
+ publisher:
6
+ class: "Roxanne::Test::Publisher"
@@ -0,0 +1,46 @@
1
+ require 'yaml'
2
+ require 'active_support/core_ext'
3
+
4
+ module Roxanne
5
+
6
+ module Configuration
7
+
8
+ class YAML < Base
9
+
10
+ def initialize(path)
11
+ super
12
+ yaml = ::YAML.load( File.open( path ) ).with_indifferent_access[:roxanne]
13
+ override_activation(yaml[:activation])
14
+ build_consumers(yaml[:consumers])
15
+ build_publisher(yaml[:publisher])
16
+ end
17
+
18
+ private
19
+ def override_activation(settings)
20
+ if settings
21
+ @active_days = settings[:days] if settings.has_key?(:days)
22
+ @timerange = settings[:timerange] if settings.has_key?(:timerange)
23
+ end
24
+ end
25
+
26
+ def build_consumers(hash)
27
+ (hash||{}).each do |id, hash|
28
+ @consumers << assign_properties( hash.delete(:class).constantize.new, hash )
29
+ end
30
+ end
31
+
32
+ def build_publisher(hash)
33
+ @publisher = assign_properties( hash.delete(:class).constantize.new, hash)
34
+ end
35
+
36
+ def assign_properties(object, hash)
37
+ hash.each do |prop, value|
38
+ object.send "#{prop}=", value
39
+ end
40
+ object
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ module Roxanne
2
+ module Configuration
3
+ class Base
4
+
5
+ attr_reader :consumers, :publisher, :active_days, :timerange
6
+
7
+ def initialize(*args)
8
+ @consumers = []
9
+ @publisher = nil
10
+ @active_days = 1..5
11
+ @timerange=8..19
12
+ end
13
+
14
+ def activated
15
+ will_activate(DateTime.now)
16
+ end
17
+
18
+ private
19
+ def will_activate( dt )
20
+ @active_days.include?(dt.wday) && @timerange.cover?(dt.hour)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ require 'roxanne/configuration/yaml'
@@ -0,0 +1,19 @@
1
+ module Roxanne
2
+ module Consumers
3
+ module Priority
4
+
5
+ def prioritize(current_status, former_status)
6
+ [:red, :orange].each do |status|
7
+ return status if status == former_status
8
+ end
9
+ current_status
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+
16
+ require 'roxanne/jenkins/consumer'
17
+ require 'roxanne/test/consumer'
18
+ require 'roxanne/travis/consumer'
19
+
@@ -0,0 +1,24 @@
1
+ require 'taopaipai'
2
+
3
+ module Roxanne
4
+ module GPIO
5
+ class Publisher
6
+ attr_accessor :green_pin, :orange_pin, :red_pin
7
+
8
+ def disable
9
+ [green_pin, orange_pin, red_pin].each do |num|
10
+ Taopaipai.gpio.pin(num, direction: :out).value 0
11
+ end
12
+ end
13
+
14
+ def push(previous, status)
15
+ return disable unless status
16
+ [:green, :orange, :red].each do |color|
17
+ Taopaipai.gpio.pin(send("#{color}_pin"), direction: :out).
18
+ value(color == status ? 1 : 0)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ require 'roxanne/http_support'
2
+
3
+ module Roxanne
4
+ module HTTP
5
+ class Consumer
6
+ include Consumers::Priority
7
+ include Roxanne::HTTPSupport
8
+
9
+ def pull
10
+ response = fetch_response
11
+ case response
12
+ when Net::HTTPSuccess
13
+ handle_response(response.body)
14
+ when Net::HTTPRedirection
15
+ puts "The request has been redirected to #{response['location']}"
16
+ :red
17
+ else
18
+ puts "The request has failed #{response.error}"
19
+ :red
20
+ end
21
+ rescue Exception => e
22
+ puts "Unable to fetch data : #{e.message}"
23
+ :red
24
+ end
25
+
26
+ def handle_response(body)
27
+ puts "Does nothing, HTTP consumer should be overriden."
28
+ :green
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ require 'net/http'
2
+
3
+ module Roxanne
4
+ module HTTPSupport
5
+ attr_accessor :host
6
+ attr_accessor :use_ssl
7
+ attr_accessor :disable_certificate_verification
8
+ attr_accessor :path
9
+ attr_accessor :port
10
+ attr_accessor :username
11
+ attr_accessor :password
12
+
13
+ private
14
+ def fetch_response
15
+ connection = ::Net::HTTP.new(@host, @port)
16
+ connection.use_ssl=@use_ssl
17
+ if @disable_certificate_verification
18
+ # TODO retrieve the certificate from the remote system, see http://redcorundum.blogspot.com/2008/03/ssl-certificates-and-nethttps.html
19
+ # Avoid issues with autosigned certificates
20
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
21
+ end
22
+ request = Net::HTTP::Get.new(complete_path, {'Accept'=>accept})
23
+
24
+ # TODO Authentication
25
+ unless (username.nil? && password.nil?)
26
+ request.basic_auth username, password
27
+ else
28
+ end
29
+ connection.request(request)
30
+ end
31
+
32
+ def complete_path
33
+ @path
34
+ end
35
+
36
+ # Override if needed to force a particular format : application/json, text/xml, etc
37
+ protected
38
+ def accept
39
+ '*'
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,34 @@
1
+ require 'roxanne/http/consumer'
2
+ require 'json'
3
+
4
+ module Roxanne
5
+ module Jenkins
6
+ class Consumer < Roxanne::HTTP::Consumer
7
+
8
+ def handle_response(body)
9
+ json = JSON.parse(body)
10
+ status = :green
11
+ json['jobs'].each do |job|
12
+ if COLORS.keys.include?(job['color'])
13
+ status = prioritize(to_status(job['color']), status)
14
+ end
15
+ end
16
+ status
17
+ end
18
+
19
+ private
20
+ COLORS = {
21
+ 'blue'=>:green,
22
+ 'yellow'=>:green,
23
+ 'red'=>:red,
24
+ 'blue_anime'=>:orange,
25
+ 'yellow_anime'=>:orange,
26
+ 'red_anime'=>:orange
27
+ }
28
+
29
+ def to_status(hudson_color)
30
+ COLORS[hudson_color]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ module Roxanne
2
+ class Loop
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def cycle
8
+ if @config.activated
9
+ status = :green
10
+ @config.consumers.each do |consumer|
11
+ actual = consumer.pull
12
+ if actual == :red
13
+ status = :red
14
+ break
15
+ elsif actual == :orange && @previous != :green
16
+ status = :orange
17
+ end
18
+ end
19
+ publish( @previous, status )
20
+ @previous = status
21
+ else
22
+ @config.publisher.disable
23
+ end
24
+ end
25
+
26
+ def reset
27
+ @config.publisher.push(nil, nil)
28
+ end
29
+
30
+ private
31
+ def publish(previous, current)
32
+ @config.publisher.push(previous, current)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,2 @@
1
+ require 'roxanne/test/publisher'
2
+ require 'roxanne/gpio/publisher'
@@ -0,0 +1,24 @@
1
+ require 'daemon_spawn'
2
+
3
+ module Roxanne
4
+ class Server < DaemonSpawn::Base
5
+
6
+ def start(args)
7
+ puts "Roxanne starting in #{self.working_dir}"
8
+ @controller = Loop.new(config(args))
9
+ loop do
10
+ @controller.cycle
11
+ sleep 5
12
+ end
13
+ end
14
+
15
+ def stop
16
+ @controller.reset
17
+ end
18
+
19
+ def config(args)
20
+ @config ||= Configuration::YAML.new(args.first || 'config/config.yml')
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module Roxanne
2
+ module Test
3
+ class Consumer
4
+ def pull
5
+ @previous = if @previous.nil?
6
+ :green
7
+ elsif @previous == :green
8
+ :red
9
+ elsif @previous == :red
10
+ :orange
11
+ else
12
+ :green
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Roxanne
2
+ module Test
3
+ class Publisher
4
+ def disable
5
+ puts "Publisher is now disabled"
6
+ end
7
+
8
+ def push(previous, status)
9
+ puts "Publisher switching from status #{default(previous)} to #{default(status)}"
10
+ end
11
+
12
+ private
13
+ def default(status)
14
+ status.nil? ? 'none' : status
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ require 'travis'
2
+
3
+ module Roxanne
4
+ module Travis
5
+ class Consumer
6
+ include Consumers::Priority
7
+
8
+ attr_accessor :organization_or_user
9
+
10
+ def pull
11
+ repos = ::Travis::Repository.find_all(owner_name: organization_or_user)
12
+ status = :green
13
+ repos.select{|repo| repo.active? }.each do |repo|
14
+ status = prioritize(to_status(repo.last_build_state), status)
15
+ end
16
+ status
17
+ end
18
+
19
+ private
20
+ STATES = {
21
+ 'failed' => :red,
22
+ 'started' => :orange,
23
+ 'passed' => :green
24
+ }
25
+
26
+ def to_status(travis_build_state)
27
+ STATES[travis_build_state]
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module Roxanne
2
+ VERSION = "0.1.0"
3
+ end
data/lib/roxanne.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "roxanne/version"
2
+
3
+ require 'roxanne/configuration'
4
+ require 'roxanne/loop'
5
+ require 'roxanne/server'
6
+ require 'roxanne/consumers'
7
+ require 'roxanne/publishers'
8
+
9
+ module Roxanne
10
+ end
data/roxanne.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'roxanne/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "roxanne"
8
+ spec.version = Roxanne::VERSION
9
+ spec.authors = ["Jef Mathiot", "Patrice Izzo", "Fabrice Nourisson",
10
+ "Benjamin Severac", "Eric Hartmann"]
11
+ spec.email = ["foss@servebox.com"]
12
+ spec.description = %q{Roxanne: publish your CI status to your device of choice}
13
+ spec.summary = %q{Aggregate the status of Continuous Integration jobs or other sources and
14
+ publish them}
15
+ spec.homepage = "http://github.com/servebox/roxanne"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files`.split($/)
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency 'json'
24
+ spec.add_dependency 'daemon-spawn'
25
+ spec.add_dependency 'activesupport', '>= 3.0.0'
26
+ spec.add_dependency 'taopaipai', '~> 0.1.1'
27
+ spec.add_dependency 'travis', '~> 1.6.8'
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.6"
30
+ spec.add_development_dependency "rake"
31
+ spec.add_development_dependency "minitest", "~> 5.0.7"
32
+ spec.add_development_dependency "minitest-implicit-subject", "~> 1.4.0"
33
+ spec.add_development_dependency "rb-readline", "~> 0.5.0"
34
+ spec.add_development_dependency "guard-minitest", "~> 2.1.3"
35
+ spec.add_development_dependency "timecop"
36
+ spec.add_development_dependency "mocha"
37
+ end
@@ -0,0 +1,21 @@
1
+ #! /bin/sh
2
+
3
+ case "$1" in
4
+ start)
5
+ su roxanne -c "bash /var/lib/roxanne/roxanne/service/roxanne.sh start" ; exit
6
+ ;;
7
+ stop)
8
+ su roxanne -c "bash /var/lib/roxanne/roxanne/service/roxanne.sh stop" ; exit
9
+ ;;
10
+ status)
11
+ ;;
12
+ restart|force-reload)
13
+ su roxanne -c "bash /var/lib/roxanne/roxanne/service/roxanne.sh restart" ; exit
14
+ ;;
15
+ *)
16
+ #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
17
+ echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
18
+ exit 3
19
+ ;;
20
+ esac
21
+
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ source $HOME/.bash_profile
3
+ eval "$(rbenv init -)"
4
+ cd /var/lib/roxanne/roxanne
5
+ bundle exec ruby lib/roxanne_server.rb $1 config/golgotha.yml