swiss_admin 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +275 -0
  6. data/Rakefile +8 -0
  7. data/bin/swissadmin +8 -0
  8. data/lib/swiss_admin.rb +9 -0
  9. data/lib/swiss_admin/cli.rb +6 -0
  10. data/lib/swiss_admin/cli/cli.rb +19 -0
  11. data/lib/swiss_admin/cli/hardware_cli.rb +19 -0
  12. data/lib/swiss_admin/cli/host_cli.rb +18 -0
  13. data/lib/swiss_admin/cli/network_cli.rb +19 -0
  14. data/lib/swiss_admin/cli/user_cli.rb +25 -0
  15. data/lib/swiss_admin/cli/web_cli.rb +68 -0
  16. data/lib/swiss_admin/hardware.rb +2 -0
  17. data/lib/swiss_admin/hardware/cpus.rb +11 -0
  18. data/lib/swiss_admin/hardware/memory.rb +36 -0
  19. data/lib/swiss_admin/host.rb +1 -0
  20. data/lib/swiss_admin/host/host.rb +15 -0
  21. data/lib/swiss_admin/network.rb +1 -0
  22. data/lib/swiss_admin/network/network.rb +13 -0
  23. data/lib/swiss_admin/user.rb +1 -0
  24. data/lib/swiss_admin/user/user.rb +18 -0
  25. data/lib/swiss_admin/version.rb +3 -0
  26. data/lib/swiss_admin/web/app.rb +60 -0
  27. data/lib/swiss_admin/web/public/css/base.css +269 -0
  28. data/lib/swiss_admin/web/public/css/layout.css +58 -0
  29. data/lib/swiss_admin/web/public/css/skeleton.css +242 -0
  30. data/lib/swiss_admin/web/public/css/tables.css +46 -0
  31. data/lib/swiss_admin/web/public/images/apple-touch-icon-114x114.png +0 -0
  32. data/lib/swiss_admin/web/public/images/apple-touch-icon-72x72.png +0 -0
  33. data/lib/swiss_admin/web/public/images/apple-touch-icon.png +0 -0
  34. data/lib/swiss_admin/web/public/images/favicon.ico +0 -0
  35. data/lib/swiss_admin/web/public/javascript/jquery-2.1.0.min.js +4 -0
  36. data/lib/swiss_admin/web/views/info.erb +60 -0
  37. data/lib/swiss_admin/web/views/layout.erb +25 -0
  38. data/swiss_admin.gemspec +29 -0
  39. data/test/cli/cli_hardware_test.rb +21 -0
  40. data/test/cli/cli_host_test.rb +20 -0
  41. data/test/cli/cli_network_test.rb +17 -0
  42. data/test/cli/cli_user_test.rb +28 -0
  43. data/test/hardware_test.rb +9 -0
  44. data/test/host_test.rb +12 -0
  45. data/test/network_test.rb +14 -0
  46. data/test/test_helper.rb +28 -0
  47. data/test/user_test.rb +17 -0
  48. data/test/web/test_api.rb +70 -0
  49. data/test/web/test_host_info.rb +44 -0
  50. metadata +216 -0
@@ -0,0 +1,25 @@
1
+ require "thor"
2
+ module SwissAdmin
3
+ module Commands
4
+ class User < Thor
5
+ namespace :user
6
+
7
+ desc "current", "Current user"
8
+ def current
9
+ $stdout.puts SwissAdmin::User.current
10
+ end
11
+
12
+ desc "home", "Current user's home directory"
13
+ def home
14
+ $stdout.puts SwissAdmin::User.home
15
+ end
16
+
17
+ desc "active", "All active users"
18
+ def active
19
+ $stdout.puts SwissAdmin::User.active[:output]
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,68 @@
1
+ require "thor"
2
+ require "swiss_admin/web/app"
3
+ module SwissAdmin
4
+ module Commands
5
+ # @todo move to another module Commands::Helpers??
6
+ def self.app_running? pid
7
+ pid = pid.to_i
8
+ result = begin
9
+ Process.kill(0, pid)
10
+ :running
11
+ rescue Errno::EPERM => e
12
+ e.message
13
+ rescue Errno::ESRCH => e
14
+ :not_running
15
+ rescue
16
+ "Unable to determine status for #{pid} : #{$!}"
17
+ end
18
+ result
19
+ end
20
+
21
+ class Web < Thor
22
+ namespace :web
23
+
24
+ desc "start", "Start web server"
25
+ # must keep global state of pid file..yaml or marshall?
26
+ # at rack start create file with pid file info
27
+ # on stop read from file then do the thing
28
+ #method_option :pid_file, aliases: "-P", desc: "Set path for pid file", default: "/tmp/swissadmin.pid"
29
+ method_option :port, aliases: "-p", desc: "Set port for web companion", default: 8080
30
+ def start
31
+ $stdout.puts "Starting..."
32
+ Rack::Server.start(app: SwissAdmin::HostInfo,
33
+ daemonize: true,
34
+ pid: options[:pid_file],
35
+ Port: options[:port])
36
+ end
37
+
38
+ # @todo need to break out all the file to separate module
39
+ desc "stop", "Stop web server"
40
+ def stop
41
+ begin
42
+ pid = IO.read("/tmp/swissadmin.pid")
43
+ res = Commands.app_running? pid
44
+ if res == :running
45
+ Process.kill("HUP",pid.to_i)
46
+ end
47
+ File.delete("/tmp/swissadmin.pid")
48
+ $stdout.puts "Stopped" || res != :running
49
+ rescue Errno::ENOENT => e
50
+ $stdout.puts e.message
51
+ return 0
52
+ end
53
+ end
54
+
55
+ desc "status", "Get status of the web server"
56
+ def status
57
+ running = false
58
+ if File.exist?("/tmp/swissadmin.pid")
59
+ pid = IO.read("/tmp/swissadmin.pid")
60
+ res = Commands.app_running? pid
61
+ running = true if res == :running
62
+ end
63
+ $stdout.puts (running ? "Running" : "Not Running")
64
+ end
65
+ end
66
+ end
67
+ end
68
+
@@ -0,0 +1,2 @@
1
+ require_relative "hardware/memory"
2
+ require_relative "hardware/cpus"
@@ -0,0 +1,11 @@
1
+ module SwissAdmin
2
+ class Hardware
3
+ def self.cpus
4
+ cpus = if File.readable?("/proc/cpuinfo")
5
+ IO.read("/proc/cpuinfo").scan(/^processor/).size
6
+ else
7
+ "unknown"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+
2
+ module SwissAdmin
3
+ class Hardware
4
+ # @example
5
+ # Mem — Displays the current state of physical RAM in the system,
6
+ # including a full breakdown of total, used, free, shared, buffered,
7
+ # and cached memory utilization in bytes.
8
+ # Swap — Displays the total, used, and free amounts of swap space, in bytes.
9
+ # MemTotal — Total amount of physical RAM, in kilobytes.
10
+ # MemFree — The amount of physical RAM, in kilobytes, left unused by the system.
11
+ # MemShared — Unused with 2.4 and higher kernels
12
+ # but left in for compatibility with earlier kernel versions.
13
+ # Buffers — The amount of physical RAM, in kilobytes, used for file buffers.
14
+ # Cached — The amount of physical RAM, in kilobytes, used as cache memory.
15
+ # Active — The total amount of buffer or page cache memory, in kilobytes, that is in active use.
16
+ # Inact_dirty — The total amount of buffer or cache pages, in kilobytes, that might be freeable.
17
+ # Inact_clean — The total amount of buffer or cache pages in kilobytes
18
+ # Inact_target — The net amount of allocations per second, in kilobytes, averaged over one minute.
19
+ # HighTotal and HighFree — The total and free amount of memory,
20
+ # respectively, that is not directly mapped into kernel space.
21
+ # The HighTotal value can vary based on the type of kernel used.
22
+ # LowTotal and LowFree — The total and free amount of memory, respectively,
23
+ # that is directly mapped into kernel space.
24
+ # The LowTotal value can vary based on the type of kernel used.
25
+ # SwapTotal — The total amount of swap available, in kilobytes.
26
+ # SwapFree — The total amount of swap free, in kilobytes.
27
+ def self.memory
28
+ memory = if File.readable?("/proc/meminfo")
29
+ IO.read("/proc/meminfo").scan(/([a-zA-Z]+):\W+(\d+)/)
30
+ else
31
+ "unknown"
32
+ end
33
+ memory.inject({}) { |a,d| a[d[0]] = d[1]; a }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1 @@
1
+ require_relative "host/host"
@@ -0,0 +1,15 @@
1
+ module SwissAdmin
2
+ class Host
3
+ def self.name
4
+ Socket.gethostname
5
+ end
6
+
7
+ def self.loadavg
8
+ loadavg = if File.readable?("/proc/loadavg")
9
+ IO.read("/proc/loadavg")
10
+ else
11
+ "unknown"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ require_relative "network/network"
@@ -0,0 +1,13 @@
1
+ require 'socket'
2
+
3
+ module SwissAdmin
4
+ class Network
5
+ def self.ip_addresses
6
+ Socket.ip_address_list.map { |i| {ip_address: i.ip_address, name: i.getnameinfo} }
7
+ end
8
+
9
+ def self.first_ipv4
10
+ Socket.ip_address_list.detect{|intf| intf.ipv4_private?}.ip_address
11
+ end
12
+ end
13
+ end
@@ -0,0 +1 @@
1
+ require_relative "user/user"
@@ -0,0 +1,18 @@
1
+ require "etc"
2
+ require "shelltastic"
3
+
4
+ module SwissAdmin
5
+ class User
6
+ def self.current
7
+ ENV["USER"] || Etc.getlogin
8
+ end
9
+
10
+ def self.home
11
+ Dir.home(Etc.getlogin)
12
+ end
13
+
14
+ def self.active
15
+ ::ShellTastic::Command.run("w -s -h").first
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module SwissAdmin
2
+ VERSION = "0.6.1"
3
+ end
@@ -0,0 +1,60 @@
1
+ require 'sinatra'
2
+ require "json"
3
+ require_relative "../hardware"
4
+ require_relative "../host"
5
+ require_relative "../network"
6
+
7
+ module SwissAdmin
8
+ class HostInfo < Sinatra::Base
9
+ helpers do
10
+ def generate_json(message)
11
+ JSON.generate(message)
12
+ end
13
+
14
+ def run_plugin(klass, params)
15
+ begin
16
+ generate_json(params[:plugin] => klass.send(params[:plugin]))
17
+ rescue NoMethodError => e
18
+ generate_json(error: "Plugin not implemented correctly or does not exist")
19
+ end
20
+ end
21
+ end
22
+
23
+ get '/test' do
24
+ "hello world"
25
+ end
26
+
27
+ get "/info" do
28
+ @memory = SwissAdmin::Hardware.memory
29
+ @host_name = SwissAdmin::Host.name
30
+ @users = SwissAdmin::User.active
31
+ @load_average = SwissAdmin::Host.loadavg
32
+ @cpus = SwissAdmin::Hardware.cpus
33
+ @ip_addresses = SwissAdmin::Network.ip_addresses
34
+ erb :info
35
+ end
36
+
37
+ #### api
38
+
39
+ ## host
40
+ get "/api/host/:plugin" do
41
+ run_plugin(SwissAdmin::Host, params)
42
+ end
43
+
44
+ ## hardware
45
+ get "/api/hardware/:plugin" do
46
+ run_plugin(SwissAdmin::Hardware, params)
47
+ end
48
+
49
+ ## User
50
+ get "/api/users/:plugin" do
51
+ run_plugin(SwissAdmin::User, params)
52
+ end
53
+
54
+ ## Network
55
+ get "/api/network/:plugin" do
56
+ run_plugin(SwissAdmin::Network, params)
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,269 @@
1
+ /*
2
+ * Skeleton V1.2
3
+ * Copyright 2011, Dave Gamache
4
+ * www.getskeleton.com
5
+ * Free to use under the MIT license.
6
+ * http://www.opensource.org/licenses/mit-license.php
7
+ * 6/20/2012
8
+ */
9
+
10
+
11
+ /* Table of Content
12
+ ==================================================
13
+ #Reset & Basics
14
+ #Basic Styles
15
+ #Site Styles
16
+ #Typography
17
+ #Links
18
+ #Lists
19
+ #Images
20
+ #Buttons
21
+ #Forms
22
+ #Misc */
23
+
24
+
25
+ /* #Reset & Basics (Inspired by E. Meyers)
26
+ ================================================== */
27
+ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
28
+ margin: 0;
29
+ padding: 0;
30
+ border: 0;
31
+ font-size: 100%;
32
+ font: inherit;
33
+ vertical-align: baseline; }
34
+ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
35
+ display: block; }
36
+ body {
37
+ line-height: 1; }
38
+ ol, ul {
39
+ list-style: none; }
40
+ blockquote, q {
41
+ quotes: none; }
42
+ blockquote:before, blockquote:after,
43
+ q:before, q:after {
44
+ content: '';
45
+ content: none; }
46
+ table {
47
+ border-collapse: collapse;
48
+ border-spacing: 0; }
49
+
50
+
51
+ /* #Basic Styles
52
+ ================================================== */
53
+ body {
54
+ background: #fff;
55
+ font: 14px/21px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
56
+ color: #444;
57
+ -webkit-font-smoothing: antialiased; /* Fix for webkit rendering */
58
+ -webkit-text-size-adjust: 100%;
59
+ }
60
+
61
+
62
+ /* #Typography
63
+ ================================================== */
64
+ h1, h2, h3, h4, h5, h6 {
65
+ color: #181818;
66
+ font-family: "Georgia", "Times New Roman", serif;
67
+ font-weight: normal; }
68
+ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-weight: inherit; }
69
+ h1 { font-size: 46px; line-height: 50px; margin-bottom: 14px;}
70
+ h2 { font-size: 35px; line-height: 40px; margin-bottom: 10px; }
71
+ h3 { font-size: 28px; line-height: 34px; margin-bottom: 8px; }
72
+ h4 { font-size: 21px; line-height: 30px; margin-bottom: 4px; }
73
+ h5 { font-size: 17px; line-height: 24px; }
74
+ h6 { font-size: 14px; line-height: 21px; }
75
+ .subheader { color: #777; }
76
+
77
+ p { margin: 0 0 20px 0; }
78
+ p img { margin: 0; }
79
+ p.lead { font-size: 21px; line-height: 27px; color: #777; }
80
+
81
+ em { font-style: italic; }
82
+ strong { font-weight: bold; color: #333; }
83
+ small { font-size: 80%; }
84
+
85
+ /* Blockquotes */
86
+ blockquote, blockquote p { font-size: 17px; line-height: 24px; color: #777; font-style: italic; }
87
+ blockquote { margin: 0 0 20px; padding: 9px 20px 0 19px; border-left: 1px solid #ddd; }
88
+ blockquote cite { display: block; font-size: 12px; color: #555; }
89
+ blockquote cite:before { content: "\2014 \0020"; }
90
+ blockquote cite a, blockquote cite a:visited, blockquote cite a:visited { color: #555; }
91
+
92
+ hr { border: solid #ddd; border-width: 1px 0 0; clear: both; margin: 10px 0 30px; height: 0; }
93
+
94
+
95
+ /* #Links
96
+ ================================================== */
97
+ a, a:visited { color: #333; text-decoration: underline; outline: 0; }
98
+ a:hover, a:focus { color: #000; }
99
+ p a, p a:visited { line-height: inherit; }
100
+
101
+
102
+ /* #Lists
103
+ ================================================== */
104
+ ul, ol { margin-bottom: 20px; }
105
+ ul { list-style: none outside; }
106
+ ol { list-style: decimal; }
107
+ ol, ul.square, ul.circle, ul.disc { margin-left: 30px; }
108
+ ul.square { list-style: square outside; }
109
+ ul.circle { list-style: circle outside; }
110
+ ul.disc { list-style: disc outside; }
111
+ ul ul, ul ol,
112
+ ol ol, ol ul { margin: 4px 0 5px 30px; font-size: 90%; }
113
+ ul ul li, ul ol li,
114
+ ol ol li, ol ul li { margin-bottom: 6px; }
115
+ li { line-height: 18px; margin-bottom: 12px; }
116
+ ul.large li { line-height: 21px; }
117
+ li p { line-height: 21px; }
118
+
119
+ /* #Images
120
+ ================================================== */
121
+
122
+ img.scale-with-grid {
123
+ max-width: 100%;
124
+ height: auto; }
125
+
126
+
127
+ /* #Buttons
128
+ ================================================== */
129
+
130
+ .button,
131
+ button,
132
+ input[type="submit"],
133
+ input[type="reset"],
134
+ input[type="button"] {
135
+ background: #eee; /* Old browsers */
136
+ background: #eee -moz-linear-gradient(top, rgba(255,255,255,.2) 0%, rgba(0,0,0,.2) 100%); /* FF3.6+ */
137
+ background: #eee -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.2)), color-stop(100%,rgba(0,0,0,.2))); /* Chrome,Safari4+ */
138
+ background: #eee -webkit-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Chrome10+,Safari5.1+ */
139
+ background: #eee -o-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Opera11.10+ */
140
+ background: #eee -ms-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* IE10+ */
141
+ background: #eee linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* W3C */
142
+ border: 1px solid #aaa;
143
+ border-top: 1px solid #ccc;
144
+ border-left: 1px solid #ccc;
145
+ -moz-border-radius: 3px;
146
+ -webkit-border-radius: 3px;
147
+ border-radius: 3px;
148
+ color: #444;
149
+ display: inline-block;
150
+ font-size: 11px;
151
+ font-weight: bold;
152
+ text-decoration: none;
153
+ text-shadow: 0 1px rgba(255, 255, 255, .75);
154
+ cursor: pointer;
155
+ margin-bottom: 20px;
156
+ line-height: normal;
157
+ padding: 8px 10px;
158
+ font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; }
159
+
160
+ .button:hover,
161
+ button:hover,
162
+ input[type="submit"]:hover,
163
+ input[type="reset"]:hover,
164
+ input[type="button"]:hover {
165
+ color: #222;
166
+ background: #ddd; /* Old browsers */
167
+ background: #ddd -moz-linear-gradient(top, rgba(255,255,255,.3) 0%, rgba(0,0,0,.3) 100%); /* FF3.6+ */
168
+ background: #ddd -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.3)), color-stop(100%,rgba(0,0,0,.3))); /* Chrome,Safari4+ */
169
+ background: #ddd -webkit-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Chrome10+,Safari5.1+ */
170
+ background: #ddd -o-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Opera11.10+ */
171
+ background: #ddd -ms-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* IE10+ */
172
+ background: #ddd linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* W3C */
173
+ border: 1px solid #888;
174
+ border-top: 1px solid #aaa;
175
+ border-left: 1px solid #aaa; }
176
+
177
+ .button:active,
178
+ button:active,
179
+ input[type="submit"]:active,
180
+ input[type="reset"]:active,
181
+ input[type="button"]:active {
182
+ border: 1px solid #666;
183
+ background: #ccc; /* Old browsers */
184
+ background: #ccc -moz-linear-gradient(top, rgba(255,255,255,.35) 0%, rgba(10,10,10,.4) 100%); /* FF3.6+ */
185
+ background: #ccc -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.35)), color-stop(100%,rgba(10,10,10,.4))); /* Chrome,Safari4+ */
186
+ background: #ccc -webkit-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Chrome10+,Safari5.1+ */
187
+ background: #ccc -o-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Opera11.10+ */
188
+ background: #ccc -ms-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* IE10+ */
189
+ background: #ccc linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* W3C */ }
190
+
191
+ .button.full-width,
192
+ button.full-width,
193
+ input[type="submit"].full-width,
194
+ input[type="reset"].full-width,
195
+ input[type="button"].full-width {
196
+ width: 100%;
197
+ padding-left: 0 !important;
198
+ padding-right: 0 !important;
199
+ text-align: center; }
200
+
201
+ /* Fix for odd Mozilla border & padding issues */
202
+ button::-moz-focus-inner,
203
+ input::-moz-focus-inner {
204
+ border: 0;
205
+ padding: 0;
206
+ }
207
+
208
+
209
+ /* #Forms
210
+ ================================================== */
211
+
212
+ form {
213
+ margin-bottom: 20px; }
214
+ fieldset {
215
+ margin-bottom: 20px; }
216
+ input[type="text"],
217
+ input[type="password"],
218
+ input[type="email"],
219
+ textarea,
220
+ select {
221
+ border: 1px solid #ccc;
222
+ padding: 6px 4px;
223
+ outline: none;
224
+ -moz-border-radius: 2px;
225
+ -webkit-border-radius: 2px;
226
+ border-radius: 2px;
227
+ font: 13px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
228
+ color: #777;
229
+ margin: 0;
230
+ width: 210px;
231
+ max-width: 100%;
232
+ display: block;
233
+ margin-bottom: 20px;
234
+ background: #fff; }
235
+ select {
236
+ padding: 0; }
237
+ input[type="text"]:focus,
238
+ input[type="password"]:focus,
239
+ input[type="email"]:focus,
240
+ textarea:focus {
241
+ border: 1px solid #aaa;
242
+ color: #444;
243
+ -moz-box-shadow: 0 0 3px rgba(0,0,0,.2);
244
+ -webkit-box-shadow: 0 0 3px rgba(0,0,0,.2);
245
+ box-shadow: 0 0 3px rgba(0,0,0,.2); }
246
+ textarea {
247
+ min-height: 60px; }
248
+ label,
249
+ legend {
250
+ display: block;
251
+ font-weight: bold;
252
+ font-size: 13px; }
253
+ select {
254
+ width: 220px; }
255
+ input[type="checkbox"] {
256
+ display: inline; }
257
+ label span,
258
+ legend span {
259
+ font-weight: normal;
260
+ font-size: 13px;
261
+ color: #444; }
262
+
263
+ /* #Misc
264
+ ================================================== */
265
+ .remove-bottom { margin-bottom: 0 !important; }
266
+ .half-bottom { margin-bottom: 10px !important; }
267
+ .add-bottom { margin-bottom: 20px !important; }
268
+
269
+