Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

adding servicebuilder

  • Loading branch information...
commit 1ed14499863870745e0ceb1a3fedb17b10a6ba9d 1 parent 29ea7fb
Erik Bourget authored
13 servicebuilder/LICENSE.md
Source Rendered
... ... @@ -0,0 +1,13 @@
  1 +Copyright 2012 Square Inc.
  2 +
  3 + Licensed under the Apache License, Version 2.0 (the "License");
  4 + you may not use this file except in compliance with the License.
  5 + You may obtain a copy of the License at
  6 +
  7 + http://www.apache.org/licenses/LICENSE-2.0
  8 +
  9 + Unless required by applicable law or agreed to in writing, software
  10 + distributed under the License is distributed on an "AS IS" BASIS,
  11 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 + See the License for the specific language governing permissions and
  13 + limitations under the License.
37 servicebuilder/README.md
Source Rendered
... ... @@ -0,0 +1,37 @@
  1 +servicebuilder
  2 +==========
  3 +
  4 +servicebuilder - a tool for building runit directory structures from
  5 +a simple YAML configuration. Tested with ruby 1.8.
  6 +
  7 +Usage
  8 +-----
  9 +<code>servicebuilder -c CONF_DIR -s STAGING_DIRECTORY -d INSTALL_DIRECTORY</code>
  10 +
  11 +Notes
  12 +-----
  13 +
  14 +The STAGING_DIRECTORY is where your service directories will be created. Runit
  15 +should *not* be monitoring this directory. To have runit notice a service, it
  16 +will be symlinked into the INSTALL_DIRECTORY. This directory should be the
  17 +directory monitored by runsvd. Runit will then begin supervising the service.
  18 +
  19 +CONF_DIR should be something like /etc/servicebuilder.d/ . It must be
  20 +filled with config files that end in ".yaml".
  21 +
  22 +Sample config file:
  23 +
  24 +<code>
  25 + mysql:
  26 + run: mysqld -fg
  27 + sleep: 30
  28 + sshd:
  29 + run: sshd -fg
  30 + log: svlogd -tt
  31 + logsleep: 5
  32 +</code>
  33 +
  34 +All "run" scripts and "log" scripts WILL BE PREPENDED WITH AN "exec"!
  35 +Your run script MUST RUN IN THE FOREGROUND or you'll create a fork bomb.
  36 +For more information about runit, see http://smarden.org/runit/ .
  37 +
328 servicebuilder/bin/servicebuilder
... ... @@ -0,0 +1,328 @@
  1 +#!/usr/bin/ruby
  2 +#
  3 +# servicebuilder - a tool for building runit directory structures from
  4 +# a simple YAML configuration. Tested with ruby 1.8.
  5 +#
  6 +# Usage:
  7 +# servicebuilder -c CONF_DIR -s STAGING_DIRECTORY -d INSTALL_DIRECTORY
  8 +#
  9 +# The STAGING_DIRECTORY is where your service directories will be created.
  10 +# Runit should *not* be monitoring this directory. To have runit notice
  11 +# a service, it will be symlinked into the INSTALL_DIRECTORY. Runit
  12 +# will then begin supervising the service.
  13 +#
  14 +# CONF_DIR should be something like /etc/servicebuilder.d/ . It must be
  15 +# filled with config files that end in ".yaml".
  16 +# Config file:
  17 +#
  18 +# mysql:
  19 +# run: mysqld -fg
  20 +# sleep: 30
  21 +# sshd:
  22 +# run: sshd -fg
  23 +# log: svlogd -tt
  24 +# logsleep: 5
  25 +#
  26 +# All "run" scripts and "log" scripts WILL BE PREPENDED WITH AN "exec"!
  27 +# Your run script MUST RUN IN THE FOREGROUND or you'll create a fork bomb.
  28 +# For more information about runit, see http://smarden.org/runit/ .
  29 +#
  30 +
  31 +require 'getoptlong'
  32 +require 'yaml'
  33 +require 'fileutils'
  34 +
  35 +# A Service has a name, a runscript, and a logscript.
  36 +# Create a Service object, populate :name, :run, :log,
  37 +# :stagedir, and :activatedir. Then, s.create! to build it in the stagedir.
  38 +# s.activate! to symlink it into the activedir.
  39 +class Service
  40 + attr_accessor :name, :run, :log, :stagedir, :activatedir, :sleep, :logsleep
  41 +
  42 + # Methods to wrap your runscript and logscript around something safe
  43 + def runscript
  44 + retstring = <<EOF
  45 +#!/usr/bin/ruby
  46 +$stderr.reopen(STDOUT)
  47 +require 'yaml'
  48 +sleep #{self.sleep}
  49 +exec *YAML.load(DATA.read)
  50 +sleep 2
  51 +__END__
  52 +#{self.run.to_yaml}
  53 +EOF
  54 + return retstring
  55 + end
  56 +
  57 + def logscript
  58 + retstring = <<EOF
  59 +#!/usr/bin/ruby
  60 +require 'yaml'
  61 +sleep #{self.logsleep}
  62 +exec *YAML.load(DATA.read)
  63 +sleep 2
  64 +__END__
  65 +#{self.log.to_yaml}
  66 +EOF
  67 + return retstring
  68 + end
  69 +
  70 + # Build the directory hierarchy in the staging directory.
  71 + def create!
  72 + modified = false
  73 + # Create directories if they need to be created
  74 + if not File.directory?(@stagedir)
  75 + puts "Creating directory #{@stagedir}"
  76 + Kernel.system("mkdir", "-p", @stagedir)
  77 + modified = true
  78 + end
  79 + if not File.directory?(File.join(@stagedir, "log"))
  80 + puts "Creating directory #{@stagedir}/log"
  81 + Kernel.system("mkdir", "-p", File.join(@stagedir, "log"))
  82 + modified = true
  83 + end
  84 + if not File.directory?(File.join(@stagedir, "log", "main"))
  85 + puts "Creating directory #{@stagedir}/log/main"
  86 + Kernel.system("mkdir", "-p", File.join(@stagedir, "log", "main"))
  87 + FileUtils.chown('nobody', 'nobody',
  88 + File.join(@stagedir, "log", "main"))
  89 + modified = true
  90 + end
  91 +
  92 + # Test to see if we need to edit the run and log/run scripts
  93 + writerun = true
  94 + if File.file?(File.join(@stagedir, "run"))
  95 + File.open(File.join(@stagedir, "run")) do |f|
  96 + currun = f.read
  97 + if(currun == self.runscript)
  98 + writerun = false
  99 + end
  100 + end
  101 + end
  102 +
  103 + writelog = true
  104 + if File.file?(File.join(@stagedir, "log", "run"))
  105 + File.open(File.join(@stagedir, "log", "run")) do |f|
  106 + curlog = f.read
  107 + if(curlog == self.logscript)
  108 + writelog = false
  109 + end
  110 + end
  111 + end
  112 +
  113 + # Write run and log/run
  114 + if writerun
  115 + File.open(File.join(@stagedir, "run"), 'w') do |f|
  116 + puts "Writing #{@stagedir}/run"
  117 + f.write(self.runscript)
  118 + end
  119 + File.chmod(0755, File.join(@stagedir, "run"))
  120 + modified = true
  121 + end
  122 +
  123 + if writelog
  124 + File.open(File.join(@stagedir, "log", "run"), 'w') do |f|
  125 + puts "Writing #{@stagedir}/log/run"
  126 + f.write(self.logscript)
  127 + end
  128 + File.chmod(0755, File.join(@stagedir, "log", "run"))
  129 + modified = true
  130 + end
  131 +
  132 + # return if we had to modify the directories
  133 + return modified
  134 + end
  135 +
  136 + # Symlink the staging directory into the active directory.
  137 + def activate!
  138 + if File.exists?(@activatedir)
  139 + if not File.symlink?(@activatedir)
  140 + puts "#{@activatedir} is not a symlink, unsure how to continue"
  141 + exit(1)
  142 + end
  143 + if not File.readlink(@activatedir) == @stagedir
  144 + puts "#{@activatedir} is not a symlink to #{@stagedir}, " +
  145 + "unsure how to continue"
  146 + exit(1)
  147 + end
  148 + else
  149 + puts "Symlinking #{@stagedir} to #{@activatedir}"
  150 + File.symlink(@stagedir, @activatedir)
  151 + end
  152 + end
  153 +end
  154 +
  155 +def usage
  156 + puts "Usage: $0 --conf CONFIG_DIRECTORY -s STAGING_DIRECTORY "
  157 + puts " -d INSTALL_DIRECTORY"
  158 + exit(1)
  159 +end
  160 +
  161 +opts = GetoptLong.new(['--confdir', '-c', GetoptLong::REQUIRED_ARGUMENT],
  162 + ['--stagedir', '-s', GetoptLong::REQUIRED_ARGUMENT],
  163 + ['--activatedir', '-d', GetoptLong::REQUIRED_ARGUMENT],
  164 + ['--help', '-h', GetoptLong::NO_ARGUMENT])
  165 +
  166 +confdir = '/etc/servicebuilder.d'
  167 +stagedir = '/var/service-stage'
  168 +activatedir = '/var/service'
  169 +
  170 +opts.each do |opt, arg|
  171 + case opt
  172 + when '--help'
  173 + usage()
  174 + when '--stagedir'
  175 + stagedir = File.expand_path(arg)
  176 + when '--activatedir'
  177 + activatedir = File.expand_path(arg)
  178 + when '--confdir'
  179 + confdir = arg
  180 + end
  181 +end
  182 +
  183 +# require these options
  184 +[confdir, stagedir, activatedir].each do |v|
  185 + usage() unless v
  186 +end
  187 +
  188 +haveservices = {}
  189 +wantservices = {}
  190 +
  191 +# read the config files, build the list of services that we want to exist
  192 +confdata = {}
  193 +raise "#{confdir} does not exist!" unless File.directory?(confdir)
  194 +Dir.open(confdir) do |dir|
  195 + dir.each do |file|
  196 + next if file =~ /^\./
  197 + next unless file =~ /\.yaml$/
  198 + configfile = File.join(confdir, file)
  199 + next unless File.file?(configfile)
  200 + config = File.read(configfile)
  201 + data = YAML.load(config)
  202 + raise "#{configfile} is empty!" if data.empty?
  203 + data.each do |k, v|
  204 + if confdata.has_key?(k)
  205 + raise "Service #{k} defined twice"
  206 + end
  207 + confdata[k] = v
  208 + end
  209 + end
  210 +end
  211 +
  212 +confdata.each do |k, d|
  213 + # build a Service object for each key in the configuration
  214 + servicename = k
  215 + raise "service #{k} has no run script" unless d.has_key?('run')
  216 +
  217 + run = d['run']
  218 + if run.class != Array
  219 + raise "Error, bad args passed to run statement"
  220 + end
  221 + sleep = 2
  222 + if d.has_key?('sleep') and d['sleep'].to_i >= 0
  223 + sleep = d['sleep'].to_i
  224 + end
  225 + logsleep = 2
  226 + if d.has_key?('logsleep') and d['logsleep'].to_i >= 0
  227 + logsleep = d['logsleep'].to_i
  228 + end
  229 + if d.member?('log')
  230 + log = d['log']
  231 + if log.class != Array
  232 + raise "Error, bad args passed to log statement"
  233 + end
  234 + else
  235 + log = %w{chpst -unobody svlogd -tt ./main}
  236 + end
  237 +
  238 + s = Service.new
  239 + s.name = servicename
  240 + s.run = run
  241 + s.log = log
  242 + s.sleep = sleep
  243 + s.logsleep = logsleep
  244 + s.stagedir = File.join(stagedir, servicename)
  245 + s.activatedir = File.join(activatedir, servicename)
  246 +
  247 + # mark that yes, we do want this service to persist
  248 + wantservices[servicename] = s
  249 +end
  250 +
  251 +# make sure /var/service exists
  252 +unless File.directory?(activatedir)
  253 + if File.exists?(activatedir)
  254 + raise "#{activatedir} exists but is not a directory, bailing"
  255 + end
  256 + FileUtils.mkdir(activatedir)
  257 +end
  258 +
  259 +# activate them by symlinking the staged service into the activate directory
  260 +wantservices.values.each do |s|
  261 + puts "Found #{s.name}"
  262 + modified = s.create!
  263 + # Restart the service if we've modified it
  264 + if modified
  265 + puts "Running sv t #{stagedir}/#{s.name}"
  266 + system("sv t #{stagedir}/#{s.name}")
  267 + puts "Warning: Unable to restart service #{s.name}" if $?
  268 + end
  269 + s.activate!
  270 +end
  271 +
  272 +# remove all services that we do not want
  273 +found_staging = []
  274 +found_activate = []
  275 +
  276 +# find every service found in the staging directory
  277 +Dir.open(stagedir) do |dir|
  278 + dir.each do |file|
  279 + next if file =~ /^\./
  280 + next unless File.directory?(File.join(stagedir, file))
  281 + found_staging.push(File.join(stagedir, file))
  282 + end
  283 +end
  284 +
  285 +# find every service found in the activate directory
  286 +Dir.open(activatedir) do |dir|
  287 + dir.each do |file|
  288 + next if file =~ /^\./
  289 + next unless File.directory?(File.join(activatedir, file))
  290 + found_activate.push(File.join(activatedir, file))
  291 + end
  292 +end
  293 +
  294 +# for each activated service, rm it if we don't want it anymore
  295 +found_activate.each do |dir|
  296 + still_wanted = false
  297 + wantservices.each do |k, v|
  298 + if v.activatedir == dir
  299 + still_wanted = true
  300 + end
  301 + end
  302 +
  303 + if still_wanted == false
  304 + puts "Unlinking #{dir} from activated location"
  305 + File.unlink(dir)
  306 + end
  307 +end
  308 +
  309 +# for each staged service, rm if we don't want it anymore
  310 +# we send sv exit only now so runit doesn't automatically restart it
  311 +# when it notices runsv not running in the activated directory
  312 +found_staging.each do |dir|
  313 + still_wanted = false
  314 + wantservices.each do |k, v|
  315 + if v.stagedir == dir
  316 + still_wanted = true
  317 + end
  318 + end
  319 +
  320 + if still_wanted == false
  321 + puts "Stopping service #{dir}"
  322 + system("sv stop #{dir}")
  323 + puts "Warning: Unable to stop service #{dir}" if $?
  324 +
  325 + puts "Removing service #{dir}"
  326 + FileUtils.rm_r(dir)
  327 + end
  328 +end
24 servicebuilder/servicebuilder.gemspec
... ... @@ -0,0 +1,24 @@
  1 +# -*- encoding: utf-8 -*-
  2 +
  3 +Gem::Specification.new do |s|
  4 + s.name = "servicebuilder"
  5 + s.version = "0.0.1"
  6 + s.platform = Gem::Platform::RUBY
  7 + s.authors = ["Erik Bourget"]
  8 + s.email = ["github@squareup.com"]
  9 + s.summary = "Tool to build runit services from simple configuration files."
  10 + s.description = "Tool to build runit services from simple configuration files."
  11 + s.homepage = "http://github.com/square/prodeng"
  12 +
  13 + s.required_rubygems_version = ">= 1.3.6"
  14 +
  15 + s.add_dependency "rdoc"
  16 + s.default_executable = %q{servicebuilder}
  17 + s.executables = %W{ servicebuilder }
  18 +
  19 +
  20 + s.files = Dir.glob("bin/*") + %w(README.md)
  21 + s.extra_rdoc_files = ["LICENSE.md"]
  22 + s.rdoc_options = ["--charset=UTF-8"]
  23 +end
  24 +

0 comments on commit 1ed1449

Please sign in to comment.
Something went wrong with that request. Please try again.