i-doit API Ruby-Scripting

Wie in meinem letzten Blogpost angekündigt stelle ich heute eine gescriptete Variante für die API der i-doit CMDB vor.

Einzelne Abfragen, sowie das Anlegen von Objekten über den RESTClient mögen auf den ersten Blick zum Testen bzw. Debuggen  ziemlich sinnvoll erscheinen, aber man stößt schnell an Grenzen, sobald es sich über mehrere hundert solcher Objekte handelt. Darüber hinaus ist diese Methode sehr umständlich.

Meine Idee war es eine Webanwendung zu benutzen, welche mit einem einzelnen Requests (im JSON-Format) an die API von I-doit Abfragen oder das Neuanlegen von Objekten vereinfacht. Da bot sich das Framework Sinatra an, da dieses eine sehr einfache und schnell zu lernende DSL besitzt. Wenn diese Anwendung läuft, kann mit der Eingabe einer bestimmten URL, ein Objekt abgefragt bzw. erstellt werden. In diesem Beispiel, werde ich die Namen sowie IDs aller Server aus der CMDB auslesen.

Zunächst müssen wir folgende drei Bibliotheken einbinden:

#!/usr/bin/env ruby

require 'sinatra/base'
require 'json'
require 'rest-client

Im nächsten Schritt bauen wir uns einen generischen Request (wie in meinem vorherigen Blogpost zu lesen) zusammen:

$baseurl = "https://example-idoit-web-gui.de"
$apikey = "random_key"

class CMDBApi
  def initialize(url=$baseurl, apikey=$apikey, objID=nil)
    @url = url
    @apikey = apikey
    @objID = objID
  end

  def postHeader(endpoint, method, payload)
    response = RestClient::Request.new({
      :method => method,
      :headers => {:accept => :json, :content_type => :json},
      :url => "#{@url}/#{endpoint}",
      :payload => payload
    }).execute
  end
  def readAllServer()
    payloadMetadata = Hash.new
    payloadMetadata[:version] = "2.0"
    payloadMetadata[:id] = "1"
    payloadMetadata[:method] = "cmdb.objects.read"
    payloadMetadata[:params] = Hash.new
    payloadMetadata[:params][:apikey] = @apikey
    payloadMetadata[:params][:filter] = {"type": "5"}
    @payloadJsonFormat = payloadMetadata.to_json
    response = postHeader("src/jsonrpc.php", :post, @payloadJsonFormat)
    response_hash = JSON.parse(response)
  end
end

get "/server" do
  api = CMDBApi.new($baseurl, $apikey)
  response = api.readAllServer(objectID)
  allServerHash = {}
  response["result"].each do |result|
    serverID = result["id"]
    serverName = result["title"]
    allServerHash[:"#{serverName}"] = serverID
  end
  allServerHash.to_json
end

Wenn man das Skript ausführt, wird (je nach Konfiguration) über http://127.0.0.1:4567 auf das Webfrontend zu gegriffen. Durch das zusätzliche Anhängen von  “/server” ist es möglich, alle Servernamen und deren zugehörigen Server-IDs aus i-doit auszulesen.

{
  • "Server_A" : 66273,
  • "Server_B" : 94647,
  • “Server_C” : 21221,
  • “…”: ….
}

Der große Vorteil einer solchen Webanwendung ist, dass man durch Erweiterung eine Automatisierung erreichen kann. Erhält man z.B. einen neuen Server, der mittels Puppet provisioniert wurde, kann vom Puppetmaster ein Objekt im i-doit angelegt werden. Hierzu kann damit auch auf alle ermittelten Facts zurückgegriffen werden. Diese sind dann über das zu erweiternde Skript an die API weiterzugeben, um final ein Server-Objekt mit allen angereicherten Informationen zu erhalten.

Philipp Dorschner
Philipp Dorschner
Junior Consultant

Philipp hat im September 2017 seine Ausbildung zum Fachinformatiker gestartet. Er hat sogar schon eine Ausbildung im Gepäck und zwar zum technischen Assistenten für Informatik. Danach hat hat er sein Abi nachgeholt und anschließend begonnen Wirtschaftswissenschaften zu studieren. Da sein Herz während des Studiums ständig nach technischen Inhalten geschrien hat, wechselte er zu Verfahrenstechnik. Aber auch dieses Studium konnte Ihn nicht erfüllen, weshalb er sich für die Ausbildung bei NETWAYS entschieden hat, "back to the...

Ruby Applikationen testen mit RSpec und WebMock

Das Testen von Software begleitet mich von Projekt zu Projekt. Bei bisherigen Entwicklungen habe ich immer wieder auf RSpec zurück gegriffen, ein tool zum testen von Ruby Applikationen. Es zeichnet sich durch eine relativ einfache Handhabung aus und eine Ausdrucksweise die an die menschliche Sprache angelehnt ist. Auch wenn nicht jeder mögliche einzutretende Fall getestet werden kann, tests geben Entwicklern Sicherheit wenn Änderungen am Code vorgenommen werden.

WebMock

WebMock ist eine Library die es einem ermöglich HTTP requests auszudrücken und Erwartungen an diese zu setzen. In Verbindung mit RSpec lässt sich WebMock verwenden um eine Ruby Anwendung zu testen die HTTP Requests ausführt, beispielsweise an eine API. Oft sind diese Requests nicht starr sondern werden dynamisch anhand von Parametern zusammen gesetzt. WebMock/RSpec hilft dabei unterschiedliche Fälle zu simulieren, Erwartungen zu definieren und diese mit zu prüfen.
WebMock ist als Gem verfügbar, die Installation ist daher relativ einfach:

user@localhost ~ $ gem install webmock

Als Beispiel verwende ich eine sehr simple Ruby Klasse. Deren einzige Methode ‘request’ sendet einen GET request an die URL ‘https://wtfismyip.com’. Abhängig vom Parameter ‘option’ wird die IP entweder in Textform oder als JSON abgefragt:

require 'net/http'
require 'uri'
class ApiCaller
  def self.request(option)
    uri = URI.parse("https://wtfismyip.com/#{option}")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    request = Net::HTTP::Get.new(uri.request_uri)
    http.request(request)
  end
end

Mein Test beinhaltet den Aufruf der ‘request’ Methode. Der HTTP request wird simuliert und die Erwartung das ein Request an eine bestimmte URL stattfinden soll wird auch definiert.

require 'api_caller'
require 'webmock/rspec'
describe ApiCaller do
  describe '.request' do
    context 'given json parameter' do
      it 'requests json url' do
        stub_request(:get, 'https://wtfismyip.com/json')
        expect(ApiCaller.request('json')).
          to have_requested(:get, 'https://wtfismyip.com/json').once
      end
    end
    context 'given text parameter' do
      it 'requests text url' do
        stub_request(:get, 'https://wtfismyip.com/text')
        expect(ApiCaller.request('text')).
          to have_requested(:get, 'https://wtfismyip.com/text').once
      end
    end
  end
end

Beim testen sieht das nun folgendermaßen aus:

user@localhost ~ $ rspec --format documentation
ApiCaller
  .request
    given json parameter
      requests json url
    given text parameter
      requests text url
Finished in 0.00557 seconds (files took 0.37426 seconds to load)
2 examples, 0 failures
Blerim Sheqa
Blerim Sheqa
Product Manager

Blerim ist seit 2013 bei NETWAYS und seitdem schon viel in der Firma rum gekommen. Neben dem Support und diversen internen Projekten hat er auch im Team Infrastruktur tatkräftig mitgewirkt. Hin und wieder lässt er sich auch den ein oder anderen Consulting Termin nicht entgehen. Inzwischen ist Blerim als Product Manager für Icinga tätig und kümmert sich dort auch um die Partner.

Clustershell und Foreman-API

i-love-apisForeman bietet die Möglichkeit verschiedene Informationen über die Hosts einzusehen. Dazu gehören der Status, das Betriebssystem, Ressourcen etc. Möchte man nun, auf mehreren Hosts gleichzeitig ein Kommando absetzen, kann man sich auf jedem einzelnen einloggen oder eine Clustershell aufbauen.
Hierfür gibt es verschiedene Tools die dies erlauben. Eine Unbequemlichkeit die hier jedoch schnell aufkommt, ist das kopieren und einfügen der Hostnamen in die Commandline. Aus diesem Grund, habe ich etwas Zeit investiert und ein Ruby Script geschrieben, das es mir ermöglicht, mit festgelegten Filtern nach speziellen Listen von Hostnamen zu suchen und diese als eine einzige Ausgabe zu speichern. Ich habe für das erzeugen von Clustershells “csshX” im Einsatz, welches ich auch direkt mit eingebunden habe.
Das get_hosts Script gibt es als GIST.
In diesem Script wird zunächst eine “config.yml” geladen, in der die Foreman-URL und der Nutzername definiert sind. Eine Passwortabfrage erfolgt in diesem Script direkt auf der Commandline. Anschließend wird die Ausgabe der Foreman-API nach dem Auflisten aller Hostinformationen in JSON geparst und alle verfügbaren Parameter für die Hosts in das entsprechende Array gespeichert. Mit dem Parameter “-s / –server” gibt man einen String an, nachdem speziell gesucht werden soll. Diese Ausgabe wird zusätzlich mit angehängt.
Gefiltert wird nach:
1) Reports enabled
2) OS Ubuntu 14.04 / Debian 8
3) Kein Match auf net-* oder netways.de (Als Beispiel)
Von den selektierten Hosts werden die Hostnamen in einer Commandline-Ausgabe mit einem Leerzeichen getrennt ausgegeben. Verschiedene werden sich eventuell fragen: “Wofür brauche ich das? Wieso sollte ich so ein Script verwenden?”
Die Antwort ist einfach: Bequemlichkeit und live Übersicht, was gerade passiert. Die Suchparameter lassen sich sehr leicht anpassen und die Ausgabe des Scriptes wird etwas an Zeit der administrativen Aufgaben sparen, vorallem dann, wenn man mehr als nur 2 oder 3 Server mit Puppet bespielen lassen möchte.
user@computer ~/Documents/ruby/foreman $ ruby script.rb
Enter password:
[ ] Trying to establish a connection...
[OK] Password correct
[OK] Connection established
[ ] Collecting data...
[OK] Data collected
[RESULTS]
Ubuntu
csshX --login root test1.test.de test2.test.de test34.test.de test19.test.de mail.test.de icinga-001.test.de
Debian
csshX --login root icinga-002.test.de db-003.test.de db-021.test.de
Finished succesfully

Wie bereits erwähnt, ist hierfür noch eine “config.yml” Datei nötig, die gewünschte Parameter enthält. In diesem Fall die URL und den usernamen. Aber auch ein Gemfile, das sich in Ruby um bestimmte Versionen von Gems kümmert. (Mit einem “bundle install” können diese installiert werden)
Die config.yml und das Gemfile gibt es ebenfalls als GIST.
Eingebaute “rescue Execptions” im Script selbst, geben entsprechende Rückmeldung, sollte der Login oder eine der auszuführenden Verarbeitungsschritte fehlschlagen und brechen den Vorgang an dieser Stelle ab.

Marius Gebert
Marius Gebert
Systems Engineer

Marius ist seit 2013 bei NETWAYS. Er hat 2016 seine Ausbildung zum Fachinformatiker für Systemintegration absolviert und ist nun im Web Services Team tätig. Hier kümmert er sich mit seinen Kollegen um die NWS Plattform und alles was hiermit zusammen hängt. 2017 hat Marius die Prüfung zum Ausbilder abgelegt und kümmert sich in seiner Abteilung um die Ausbildung unserer jungen Kollegen. Seine Freizeit verbringt Marius gerne an der frischen Luft und ist für jeden Spaß zu...

May I introduce the Rubocop

When you are into developing Ruby code, or even Ruby near stuff like Puppet modules, or Chef cookbooks, there is a nice tool you should have a look at.
The RuboCop can help you writing better Ruby code, it certainly did it for me.
In short words, RubyCop is a code analyzer that checks Ruby code against common style guidelines and tries to detect a lot of mistakes and errors that you might write into your code.
There are a lot of configuration options, and even an auto-correct functionality, that updates your code.

Simple usage

Either install the gem, or add it to your Gemfile:

gem 'rubocop', require: false

You can just run it without configuration, and it will look for all Ruby files in your work directory.

$ rubocop
Inspecting 19 files
....C............CC
Offenses:
lib/test/cli.rb:3:3: C: Missing top-level class documentation comment.
 class CLI
 ^^^^^
lib/test/cli.rb:36:1: C: Extra empty line detected at method body beginning.
lib/test/cli.rb:41:1: C: Extra empty line detected at block body beginning.
lib/test/cli.rb:45:4: C: Final newline missing.
end
bin/test:12:1: C: Missing space after #.
#api.login('username', 'Passw0rd') unless api.logged_in?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
bin/test:15:3: C: Missing space after #.
 #puts response.code
 ^^^^^^^^^^^^^^^^^^^
bin/test:19:1: C: Missing space after #.
#session.save(file)
^^^^^^^^^^^^^^^^^^^
18 files inspected, 7 offenses detected

You can add it as a rake job to your Rakefile:

require 'rubocop/rake_task'
RuboCop::RakeTask.new
task default: [:spec, :rubocop]

And run the test via your rake tests:

$ rake
$ rake rubocop

Configuration galore

There are a lot of options to modify the behavior and expectations of RuboCop.
Here is a short example I used with recent Puppet module.

require: rubocop-rspec
AllCops:
  TargetRubyVersion: 1.9
  Include:
    - ./**/*.rb
  Exclude:
    - vendor/**/*
    - .vendor/**/*
    - pkg/**/*
    - spec/fixtures/**/*
# We don't use rspec in this way
RSpec/DescribeClass:
  Enabled: False
RSpec/ImplicitExpect:
  Enabled: False
# Example length is not necessarily an indicator of code quality
RSpec/ExampleLength:
  Enabled: False
RSpec/NamedSubject:
  Enabled: False

Where to go next

Markus Frosch
Markus Frosch
Principal Consultant

Markus arbeitet bei NETWAYS als Principal Consultant und unterstützt Kunden bei der Implementierung von Nagios, Icinga und anderen Open Source Systems Management Tools. Neben seiner beruflichen Tätigkeit ist Markus aktiver Mitarbeiter im Debian Projekt.

Puppet Hash Injection

forge-logoFolgende Ausgangssituation, wir wollen eine Konfigurationsdatei zeilenweise mit beliebigen Optionen und den zugehörigen Werten verwalten. Hierzu benutzen wir eine Defined Resource, um jeweils einen Eintrag zu konfiguration:

define config::setting(
  $option = $title,
  $value,
) {
  file_line { "config::setting::${option}":
    line => "${option} = ${value}",
    ...
}

Nun möchten wir jedoch aus einer Klasse heraus, einen einfachen Hash zur Deklaration verwenden und nicht einen Hash of Hashes.

class { 'config':
  options => { 'opt1' => 'alpha', 'opt2' => 'beta' },
}

Das Problem ist nun, dass wir aus options einen Hash der folgenden Form machen müssen, um mit create_resources, opt1 und opt2 anlegen zu können.

{ 'opt1' => { 'value' => 'alpha' },
  'opt2' => { 'value' => 'beta' },
}

Ruby bietet für Hashes eine Methode inject, die wir innerhalb eines inline_templates anwenden. Die inline_template Funktion liefert jedoch nur Strings zurück und keinen Hash. Deshalb geben wir den String in YAML zurück, den wir abschließend mit der im Module puppetlabs-stdlib enthaltenen Funktion parseyaml wieder in einen Hash der geforderten Form umwandeln.

class config(
  $options = {},
) {
  create_resources('config::setting',
    parseyaml(inline_template(
      '<%= @options.inject({}) {|h, (x,y)| h[x] = {"option" => x, "value" => y}; h}.to_yaml %>'))
  )
}
Lennart Betz
Lennart Betz
Senior Consultant

Der diplomierte Mathematiker arbeitet bei NETWAYS im Bereich Consulting und bereichert seine Kunden mit seinem Wissen zu Icinga, Nagios und anderen Open Source Administrationstools. Im Büro erleuchtet Lennart seine Kollegen mit fundierten geschichtlichen Vorträgen die seinesgleichen suchen.

Paketstation

Mit FPM stellt Jordan Sissel, der führende Kopf hinter Logstash, ein Tool zur Verfügung mit dem man sehr einfach Pakete für die gängigsten Distributionen erstellen kann.
Für Effing Package Management (=FPM) muss Ruby und ein C-Compiler vorhanden sein:

# apt-get install ruby-dev gcc
# yum install ruby-devel gcc

Danach kann die eigentliche Installation des Ruby-Moduls erfolgen:

# gem install fpm

Als Beispiel für die Erstellung von Paketen mit FPM habe ich im Folgenden LConf Standalone Web gewählt, natürlich kann dies auch auf andere Softwareprodukte abgewandelt werden.
Zuerst muss bei LConf Web das Archiv mit dem Quellcode herunter geladen und entpackt werden, danach wechselt man wie gewohnt in das extrahierte Verzeichnis. Im Anschluss wird hier der configure-Befehl ausgeführt:

# ./configure --with-user=icinga --with-group=icinga

Bis zu diesem Zeitpunkt besteht noch kein Unterschied zum üblichen Installationsvorgang, erst bei make install unterscheidet sich der Aufruf. Statt die Dateien an den dafür vorgesehenen Stellen im Dateisystem abzulegen, leitet man sie mit dem DESTDIR-Parameter um:

# make install DESTDIR=/usr/local/src/lconf-web-1.4.0_installdir

Jetzt ist die Zeit von FPM gekommen, den hier kann nun das DESTDIR-Verzeichnis als Chroot verwendet und die darin enthaltene Verzeichnisstruktur “usr/local/lconf-web” beispielsweise in ein Debian-Paket integriert werden:

# fpm -s dir -t deb -n lconf-web -v 1.4.0 --deb-use-file-permissions \
-C /usr/local/src/lconf-web-1.4.0_installdir usr/local/lconf-web

Das dabei entstandene Paket lconf-web-1.4.0_amd64.deb kann nun natürlich mit dpkg auf einem Debian basierten System installiert werden.

Markus Waldmüller
Markus Waldmüller
Lead Senior Consultant

Markus war bereits mehrere Jahre als Sysadmin in Neumarkt i.d.OPf. und Regensburg tätig. Nach Technikerschule und Selbständigkeit ist er nun Anfang 2013 bei NETWAYS als Lead Senior Consultant gelandet. Wenn er nicht gerade die Welt bereist, ist der sportbegeisterte Neumarkter mit an Sicherheit grenzender Wahrscheinlichkeit auf dem Mountainbike oder am Baggersee zu finden.