├── .github └── CODEOWNERS ├── .gitignore ├── .rspec ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── README.adoc ├── Rakefile ├── app ├── controllers │ ├── alerting_controller.rb │ ├── application_controller.rb │ ├── authenticated_users_controller.rb │ ├── clusters_controller.rb │ ├── jobs_controller.rb │ ├── nodes_controller.rb │ ├── notifications_controller.rb │ ├── ping_controller.rb │ ├── sessions_controller.rb │ └── users_controller.rb ├── forms │ └── user_form.rb ├── models │ ├── alert.rb │ ├── brick.rb │ ├── cluster.rb │ ├── job.rb │ ├── node.rb │ ├── notification.rb │ ├── user.rb │ └── volume.rb └── presenters │ ├── brick_presenter.rb │ ├── cluster_presenter.rb │ ├── job_presenter.rb │ ├── node_presenter.rb │ ├── notification_presenter.rb │ ├── user_presenter.rb │ └── volume_presenter.rb ├── config.ru ├── config ├── apache.vhost-ssl.sample ├── apache.vhost.sample ├── etcd.sample.yml ├── initializers │ └── etcd.rb ├── puma │ └── production.rb ├── tendrl-api_logrotate.conf └── tendrl.sample.yml ├── dependencies ├── etcd.spec ├── puma.spec ├── rubygem-bundler.spec ├── rubygem-minitest.spec ├── rubygem-mixlib-log.spec ├── rubygem-sinatra.spec └── rubygem-tilt.spec ├── docs ├── alerts.adoc ├── authentication.adoc ├── authorizaton.adoc ├── clusters.adoc ├── jobs.adoc ├── nodes.adoc ├── notifications.adoc ├── overview.adoc ├── users.adoc └── volumes.adoc ├── firewalld └── tendrl-api.xml ├── lib ├── tendrl.rb └── tendrl │ ├── atom.rb │ ├── attribute.rb │ ├── errors │ ├── invalid_object_error.rb │ └── tendrl_error.rb │ ├── flow.rb │ ├── http_response_error_handler.rb │ ├── object.rb │ └── version.rb ├── public └── .keep ├── spec ├── controllers │ ├── authenticated_users_controller_spec.rb │ ├── clusters_controller_spec.rb │ ├── jobs_controller_spec.rb │ ├── nodes_controller_spec.rb │ ├── sessions_controller_spec.rb │ └── users_controller_spec.rb ├── fixtures │ ├── cluster.json │ ├── cluster_context.json │ ├── clusters.yml │ ├── create_user.json │ ├── definitions │ │ ├── ceph.yaml │ │ ├── ceph_definitions.json │ │ ├── gluster.yaml │ │ ├── master.yaml │ │ └── tendrl_definitions_node_agent.yaml │ ├── detected_cluster.json │ ├── dwarner.json │ ├── ip.json │ ├── job.yml │ ├── job_created.json │ ├── jobs.json │ ├── managed_clusters.json │ ├── node.json │ ├── node_ids.json │ ├── nodes.json │ ├── pools.json │ ├── quentin.json │ ├── thardy.json │ ├── unmanaged_clusters.json │ ├── users.yml │ └── volumes.json ├── forms │ └── user_form_spec.rb ├── models │ └── user_spec.rb ├── spec_helper.rb ├── tendrl │ └── flow_spec.rb └── tendrl_spec.rb ├── tendrl-api.service └── tendrl-api.spec /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. Unless a later match takes precedence, 6 | # @global-owner1 and @global-owner2 will be requested for 7 | # review when someone opens a pull request. 8 | * @Tendrl/admins @Tendrl/api 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config/etcd.yml 2 | config/tendrl.yml 3 | *.log 4 | .DS_Store 5 | config/sds 6 | coverage 7 | tmp 8 | log 9 | *.swp 10 | *.swo 11 | .bundle 12 | vendor/bundle 13 | vendor/bin 14 | config/gluster.yaml 15 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | -c 2 | -fd 3 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | tendrl-api 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0-p598 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tendrl/api/b7ce23e515e5359fc5198ad303b677cd1b6dad69/.travis.yml -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.0.0' 4 | 5 | gem 'sinatra', '1.4.5', require: 'sinatra/base' 6 | gem 'etcd', '0.3.0' 7 | gem 'activesupport', "4.2.6", require: false 8 | gem 'activemodel', '4.2.6', require: 'active_model' 9 | gem 'rake', '0.9.6' 10 | gem 'puma', '3.6.0' 11 | gem 'bcrypt', '3.1.11' 12 | 13 | group :development do 14 | gem 'rubocop', require: false 15 | gem 'shotgun' 16 | gem 'rb-readline' 17 | end 18 | 19 | group :test do 20 | gem 'rspec' 21 | gem 'rack-test' 22 | gem 'webmock' 23 | gem 'simplecov', :require => false 24 | gem 'rest-client' 25 | end 26 | 27 | 28 | group :documentation do 29 | gem 'asciidoctor' 30 | end 31 | 32 | gem 'ruby-prof', require: false 33 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activemodel (4.2.6) 5 | activesupport (= 4.2.6) 6 | builder (~> 3.1) 7 | activesupport (4.2.6) 8 | i18n (~> 0.7) 9 | json (~> 1.7, >= 1.7.7) 10 | minitest (~> 5.1) 11 | thread_safe (~> 0.3, >= 0.3.4) 12 | tzinfo (~> 1.1) 13 | addressable (2.5.0) 14 | public_suffix (~> 2.0, >= 2.0.2) 15 | asciidoctor (1.5.5) 16 | ast (2.3.0) 17 | bcrypt (3.1.11) 18 | builder (3.2.3) 19 | crack (0.4.3) 20 | safe_yaml (~> 1.0.0) 21 | diff-lcs (1.2.5) 22 | docile (1.1.5) 23 | domain_name (0.5.20170223) 24 | unf (>= 0.0.5, < 1.0.0) 25 | etcd (0.3.0) 26 | mixlib-log 27 | hashdiff (0.3.1) 28 | http-cookie (1.0.3) 29 | domain_name (~> 0.5) 30 | i18n (0.7.0) 31 | json (1.8.3) 32 | mime-types (3.1) 33 | mime-types-data (~> 3.2015) 34 | mime-types-data (3.2016.0521) 35 | minitest (5.9.1) 36 | mixlib-log (1.7.1) 37 | netrc (0.11.0) 38 | parser (2.3.1.4) 39 | ast (~> 2.2) 40 | powerpack (0.1.1) 41 | public_suffix (2.0.4) 42 | puma (3.6.0) 43 | rack (1.6.4) 44 | rack-protection (1.5.3) 45 | rack 46 | rack-test (0.6.3) 47 | rack (>= 1.0) 48 | rainbow (2.1.0) 49 | rake (0.9.6) 50 | rb-readline (0.5.5) 51 | rest-client (2.0.1) 52 | http-cookie (>= 1.0.2, < 2.0) 53 | mime-types (>= 1.16, < 4.0) 54 | netrc (~> 0.8) 55 | rspec (3.5.0) 56 | rspec-core (~> 3.5.0) 57 | rspec-expectations (~> 3.5.0) 58 | rspec-mocks (~> 3.5.0) 59 | rspec-core (3.5.4) 60 | rspec-support (~> 3.5.0) 61 | rspec-expectations (3.5.0) 62 | diff-lcs (>= 1.2.0, < 2.0) 63 | rspec-support (~> 3.5.0) 64 | rspec-mocks (3.5.0) 65 | diff-lcs (>= 1.2.0, < 2.0) 66 | rspec-support (~> 3.5.0) 67 | rspec-support (3.5.0) 68 | rubocop (0.44.1) 69 | parser (>= 2.3.1.1, < 3.0) 70 | powerpack (~> 0.1) 71 | rainbow (>= 1.99.1, < 3.0) 72 | ruby-progressbar (~> 1.7) 73 | unicode-display_width (~> 1.0, >= 1.0.1) 74 | ruby-prof (0.16.2) 75 | ruby-progressbar (1.8.1) 76 | safe_yaml (1.0.4) 77 | shotgun (0.9.2) 78 | rack (>= 1.0) 79 | simplecov (0.13.0) 80 | docile (~> 1.1.0) 81 | json (>= 1.8, < 3) 82 | simplecov-html (~> 0.10.0) 83 | simplecov-html (0.10.0) 84 | sinatra (1.4.5) 85 | rack (~> 1.4) 86 | rack-protection (~> 1.4) 87 | tilt (~> 1.3, >= 1.3.4) 88 | thread_safe (0.3.5) 89 | tilt (1.4.1) 90 | tzinfo (1.2.2) 91 | thread_safe (~> 0.1) 92 | unf (0.1.4) 93 | unf_ext 94 | unf_ext (0.0.7.2) 95 | unicode-display_width (1.1.1) 96 | webmock (2.3.1) 97 | addressable (>= 2.3.6) 98 | crack (>= 0.3.2) 99 | hashdiff 100 | 101 | PLATFORMS 102 | ruby 103 | 104 | DEPENDENCIES 105 | activemodel (= 4.2.6) 106 | activesupport (= 4.2.6) 107 | asciidoctor 108 | bcrypt (= 3.1.11) 109 | etcd (= 0.3.0) 110 | puma (= 3.6.0) 111 | rack-test 112 | rake (= 0.9.6) 113 | rb-readline 114 | rest-client 115 | rspec 116 | rubocop 117 | ruby-prof 118 | shotgun 119 | simplecov 120 | sinatra (= 1.4.5) 121 | webmock 122 | 123 | RUBY VERSION 124 | ruby 2.0.0p598 125 | 126 | BUNDLED WITH 127 | 1.13.6 128 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := tendrl-api 2 | VERSION := 1.6.3 3 | RELEASE := 7 4 | COMMIT := $(shell git rev-parse HEAD) 5 | SHORTCOMMIT := $(shell echo $(COMMIT) | cut -c1-7) 6 | 7 | all: srpm 8 | 9 | dist: 10 | mkdir -p $(NAME)-$(VERSION) 11 | cp -r app config docs firewalld lib public spec $(NAME)-$(VERSION)/ 12 | cp config.ru LICENSE Gemfile* Makefile Rakefile README* tendrl-api.* $(NAME)-$(VERSION)/ 13 | tar -zcf $(NAME)-$(VERSION).tar.gz $(NAME)-$(VERSION) 14 | rm -rf $(NAME)-$(VERSION) 15 | 16 | clean: 17 | rm -f $(NAME)-$(VERSION).tar.gz 18 | rm -f $(NAME)-$(VERSION)*.rpm 19 | rm -f *.log 20 | 21 | srpm: dist 22 | fedpkg --dist epel7 srpm 23 | 24 | rpm: srpm 25 | mock -r epel-7-x86_64 rebuild $(NAME)-$(VERSION)-$(RELEASE).el7.src.rpm --resultdir=. --define "dist .el7" 26 | 27 | gitversion: 28 | sed -i $(NAME).spec \ 29 | -e "/^Release:/cRelease: $(shell date +"%Y%m%dT%H%M%S").$(SHORTCOMMIT)" 30 | 31 | snapshot: gitversion srpm 32 | 33 | .PHONY: dist rpm srpm gitversion snapshot 34 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | //vim: tw=79 2 | :sectanchors: 3 | :sectlinks: 4 | :toc: 5 | 6 | = Tendrl API 7 | 8 | == Build Status 9 | 10 | - Unit tests: image:https://travis-ci.org/Tendrl/api.svg?branch=master["Build Status", link="https://travis-ci.org/Tendrl/api"] 11 | - Functional tests: image:https://ci.centos.org/view/Tendrl-master/job/tendrl-build-1-master-api/lastBuild/badge/icon["Build Status", link="https://ci.centos.org/view/Tendrl-master/job/tendrl-build-1-master-api/lastBuild/"] 12 | 13 | == Installation from Source on CentOS 7 14 | 15 | NOTE: All the commands are run as a regular user that has `sudo` privileges. 16 | The commands are all assumed to be run from a single directory, which by 17 | default could be the user's home directory. If different, the required current 18 | directory is indicated in `[]` before the shell prompt `$`. 19 | 20 | === Deployment Requirements 21 | 22 | Ensure that etcd is running on a node in the network and is reachable from the 23 | node you're about to install tendrl-api on. Note it's address and port. In most 24 | development setups, both etcd and tendrl-api would reside on the same host. 25 | 26 | === System Setup 27 | 28 | . Install the build toolchain. 29 | 30 | $ sudo yum groupinstall 'Development Tools' 31 | 32 | . Install Ruby 2.0.0p598. 33 | 34 | $ sudo yum install ruby ruby-devel rubygem-bundler 35 | 36 | === Install tendrl-api 37 | 38 | . Clone tendrl-api. 39 | 40 | $ git clone https://github.com/Tendrl/tendrl-api.git 41 | 42 | . Install the gem dependencies, either.. 43 | 44 | $ cd tendrl-api 45 | 46 | .. everything, 47 | 48 | [tendrl-api] $ bundle install --path vendor/bundle --binstubs vendor/bin 49 | 50 | .. OR development setup only, 51 | 52 | [tendrl-api] $ bundle install --path vendor/bundle --binstubs vendor/bin \ 53 | --without production 54 | 55 | .. OR production setup only. 56 | 57 | [tendrl-api] $ bundle install --path vendor/bundle --binstubs vendor/bin \ 58 | --without development test documentation 59 | 60 | NOTE: Using binstubs allows any of the executables to be executed directly from 61 | `vendor/bin`, instead of via `bundle exec`. 62 | 63 | === Configuration 64 | 65 | To configure the etcd connection information, copy the sample configuration 66 | file to the appropriate location and make the necessary changes based on your 67 | etcd configuration, as discussed in the <<_deployment_requirements,Deployment 68 | Requirements>> section. 69 | 70 | [tendrl-api] $ cp config/etcd.sample.yml config/etcd.yml 71 | 72 | 73 | == Development Environment 74 | 75 | NOTE: All the commands below are assumed to be run from inside the git checkout 76 | directory. 77 | 78 | . Tendrl Definitions: 79 | + 80 | The API needs the proper Tendrl definitions yaml file to generate the 81 | attributes and actions. You can either download it or use the one from the 82 | fixtures to explore the API. 83 | 84 | [tendrl-api] $ cp spec/fixtures/sds/tendrl_definitions_gluster-3.8.3.yaml \ 85 | config/sds/tendrl_definitions_gluster-3.8.3.yaml 86 | 87 | . Seed the etcd instance (optional): 88 | + 89 | The script will seed the etcd instance with mock cluster data and print a 90 | cluster uuid which can be used to make API requests. 91 | 92 | [tendrl-api] $ vendor/bin/rake etcd:seed # Seed the local store with cluster 93 | 94 | . Start the development server: 95 | + 96 | This server will reload itself when any of the source files are updated. 97 | 98 | [tendrl-api] $ vendor/bin/shotgun 99 | + 100 | NOTE: This makes the development server to be queryable on `localhost:9393` by 101 | default. Check `vendor/bin/shotgun --help` to change the ip:port binding. 102 | 103 | 104 | == Test Environment 105 | 106 | The test environment does not need the local etcd instance to run the tests. 107 | 108 | [tendrl-api] $ vendor/bin/rspec 109 | 110 | 111 | == Running on Port 80 112 | 113 | Binding to port 80 requires root permissions. However, tendrl-api runs as a 114 | normal user. In order to make the application available on port 80, apache 115 | needs to be installed and configured. 116 | 117 | . Install apache 118 | 119 | $ sudo yum install httpd 120 | 121 | . Copy over the sample configuration file and validate it's syntax. 122 | + 123 | IMPORTANT: Update the file for your specific host details. The file is 124 | commented to point out the suggested changes. The file is configured to connect 125 | to the tendrl-api application server on port 9292. 126 | + 127 | IMPORTANT: Running behind apache makes the API available at 128 | `http://:80/api/`. Client applications' (including tendrl frontend's) 129 | configuration needs to be updated to make all API queries behind this endpoint. 130 | 131 | [tendrl-api] $ sudo cp config/apache.vhost.sample \ 132 | /etc/httpd/conf.d/tendrl.conf 133 | $ sudo apachectl configtest 134 | 135 | . Update the SELinux configuration to allow apache to make connections. 136 | 137 | $ sudo setsebool -P httpd_can_network_connect 1 138 | 139 | . Run the application via the production server `puma`, daemonised, listening 140 | on port 9292. 141 | 142 | [tendrl-api] $ vendor/bin/puma -e development -d 143 | + 144 | NOTE: It is possible to run both the development and the production servers at 145 | the same time, with the production server behind apache. While the production 146 | server `puma` runs, by default, on port 9292; the development server `shotgun` 147 | listens on port 9393. 148 | 149 | . Start apache. 150 | 151 | $ sudo systemctl start httpd.service 152 | 153 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require './lib/tendrl' 2 | require 'etcd' 3 | require 'yaml' 4 | require 'json' 5 | require 'securerandom' 6 | 7 | namespace :etcd do 8 | desc 'Load default Tendrl admin in etcd' 9 | task :load_admin do 10 | p 'Generating default Tendrl admin' 11 | password = 'adminuser' 12 | user = Tendrl::User.find 'admin' 13 | if user 14 | p 'User named admin already exists.' 15 | else 16 | Tendrl::User.save( 17 | Tendrl::UserForm.new( 18 | Tendrl::User.new, 19 | name: 'Admin', 20 | username: 'admin', 21 | email: 'admin@tendrl.org', 22 | role: 'admin', 23 | password: password, 24 | email_notifications: false 25 | ).attributes 26 | ) 27 | p 'Generated default admin' 28 | p 'Username: admin' 29 | p "Password: #{password}" 30 | end 31 | end 32 | end 33 | 34 | if ENV['RACK_ENV'] != 'production' 35 | require 'rspec/core/rake_task' 36 | RSpec::Core::RakeTask.new :specs do |task| 37 | task.pattern = Dir['spec/**/*_spec.rb'] 38 | end 39 | task :default => ['specs'] 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/alerting_controller.rb: -------------------------------------------------------------------------------- 1 | class AlertingController < AuthenticatedUsersController 2 | 3 | get '/alerts' do 4 | begin 5 | alerts = Tendrl::Alert.all 6 | alerts.to_json 7 | rescue Etcd::KeyNotFound 8 | [].to_json 9 | end 10 | end 11 | 12 | get '/alerts/:alert_id' do 13 | Tendrl::Alert.find(params[:alert_id]).to_json 14 | end 15 | 16 | get '/clusters/:cluster_id/alerts' do 17 | Tendrl::Alert.all("clusters/#{params[:cluster_id]}").to_json 18 | end 19 | 20 | get '/nodes/:node_id/alerts' do 21 | Tendrl::Alert.all("nodes/#{params[:node_id]}").to_json 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < Sinatra::Base 2 | set :root, File.dirname(__FILE__) 3 | 4 | set :environment, ENV['RACK_ENV'] 5 | 6 | set :logging, true 7 | 8 | set :logging, ENV['LOG_LEVEL'] || Logger::DEBUG 9 | 10 | set :http_allow_methods, [ 11 | 'POST', 12 | 'GET', 13 | 'OPTIONS', 14 | 'PUT', 15 | 'DELETE' 16 | ] 17 | 18 | set :http_allow_headers, [ 19 | 'Origin', 20 | 'Content-Type', 21 | 'Accept', 22 | 'Authorization', 23 | 'X-Requested-With' 24 | ] 25 | 26 | set :http_allow_origin, [ 27 | '*' 28 | ] 29 | 30 | error Etcd::NotDir do 31 | halt 404, { errors: { message: 'Not found.' }}.to_json 32 | end 33 | 34 | error Etcd::KeyNotFound do 35 | exception = Tendrl::HttpResponseErrorHandler.new(env['sinatra.error']) 36 | halt exception.status, exception.body.to_json 37 | end 38 | 39 | error JSON::ParserError do 40 | exception = Tendrl::HttpResponseErrorHandler.new( 41 | env['sinatra.error'], 42 | cause: 'invalid_json' 43 | ) 44 | halt exception.status, exception.body.to_json 45 | end 46 | 47 | error Errno::ETIMEDOUT do 48 | exception = Tendrl::HttpResponseErrorHandler.new( 49 | env['sinatra.error'], 50 | cause: 'etcd_timeout' 51 | ) 52 | halt exception.status, exception.body.to_json 53 | end 54 | 55 | error Errno::ECONNREFUSED do 56 | exception = Tendrl::HttpResponseErrorHandler.new( 57 | env['sinatra.error'], 58 | cause: 'etcd_not_reachable' 59 | ) 60 | halt exception.status, exception.body.to_json 61 | end 62 | 63 | error do 64 | exception = Tendrl::HttpResponseErrorHandler.new( 65 | env['sinatra.error'], 66 | cause: 'uncaught_exception' 67 | ) 68 | halt exception.status, exception.body.to_json 69 | end 70 | 71 | 72 | before do 73 | content_type :json 74 | response.headers["Access-Control-Allow-Origin"] = 75 | settings.http_allow_origin.join(',') 76 | response.headers["Access-Control-Allow-Methods"] = 77 | settings.http_allow_methods.join(',') 78 | response.headers["Access-Control-Allow-Headers"] = 79 | settings.http_allow_headers.join(',') 80 | end 81 | 82 | options '*' do 83 | status 200 84 | end 85 | 86 | protected 87 | 88 | def access_token 89 | token = nil 90 | if request.env['HTTP_AUTHORIZATION'] 91 | token = request.env['HTTP_AUTHORIZATION'].split('Bearer ')[-1] 92 | end 93 | token 94 | end 95 | 96 | def etcd 97 | Tendrl.etcd 98 | end 99 | 100 | end 101 | -------------------------------------------------------------------------------- /app/controllers/authenticated_users_controller.rb: -------------------------------------------------------------------------------- 1 | class AuthenticatedUsersController < ApplicationController 2 | 3 | before do 4 | authenticate 5 | end 6 | 7 | before do 8 | if ['POST', 'PUT', 'DELETE'].include? request.request_method 9 | unless users_path? 10 | halt 403, { errors: { message: 'Forbidden' } }.to_json if limited_user? 11 | end 12 | end 13 | end 14 | 15 | get '/current_user' do 16 | UserPresenter.single(current_user).to_json 17 | end 18 | 19 | def users_path? 20 | request.path_info.match(/\/users\/.+/).present? 21 | end 22 | 23 | def admin_user? 24 | current_user.admin? 25 | end 26 | 27 | def normal_user? 28 | current_user.normal? 29 | end 30 | 31 | def limited_user? 32 | current_user.limited? 33 | end 34 | 35 | error Tendrl::HttpResponseErrorHandler do 36 | halt env['sinatra.error'].status, env['sinatra.error'].body.to_json 37 | end 38 | 39 | protected 40 | 41 | def authenticate 42 | if access_token.present? 43 | @current_user = Tendrl::User.authenticate_access_token(access_token) 44 | end 45 | halt 401, { errors: { message: 'Unauthorized'} }.to_json if @current_user.nil? 46 | end 47 | 48 | def current_user 49 | @current_user || authenticate 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/controllers/clusters_controller.rb: -------------------------------------------------------------------------------- 1 | class ClustersController < AuthenticatedUsersController 2 | get '/clusters' do 3 | clusters = Tendrl::Cluster.all 4 | { clusters: ClusterPresenter.list(clusters) }.to_json 5 | end 6 | 7 | get '/clusters/:cluster_id' do 8 | cluster = Tendrl::Cluster.find(params[:cluster_id]) 9 | status 200 10 | ClusterPresenter.single( 11 | params[:cluster_id] => cluster.attributes 12 | ).to_json 13 | end 14 | 15 | get '/clusters/:cluster_id/nodes' do 16 | nodes = Tendrl::Node.find_all_by_cluster_id(params[:cluster_id]) 17 | node_list = NodePresenter.list(nodes).map do |node_data| 18 | bricks = Tendrl::Brick.find_all_by_cluster_id_and_node_fqdn( 19 | params[:cluster_id], node_data['fqdn'] 20 | ) 21 | node_data.merge(bricks_count: bricks.size) 22 | end 23 | { nodes: node_list }.to_json 24 | end 25 | 26 | get '/clusters/:cluster_id/nodes/:node_id/bricks' do 27 | node = Tendrl::Node.find_by_cluster_id( 28 | params[:cluster_id], params[:node_id] 29 | ) 30 | halt 404 unless node.present? 31 | bricks = Tendrl::Brick.find_all_by_cluster_id_and_node_fqdn( 32 | params[:cluster_id], node['fqdn'] 33 | ) 34 | { bricks: BrickPresenter.list(bricks) }.to_json 35 | end 36 | 37 | get '/clusters/:cluster_id/volumes' do 38 | volumes = Tendrl::Volume.find_all_by_cluster_id(params[:cluster_id]) 39 | { volumes: VolumePresenter.list(volumes) }.to_json 40 | end 41 | 42 | get '/clusters/:cluster_id/volumes/:volume_id/bricks' do 43 | references = Tendrl::Brick.find_refs_by_cluster_id_and_volume_id( 44 | params[:cluster_id], params[:volume_id] 45 | ) 46 | bricks = Tendrl::Brick.find_by_cluster_id_and_refs(params[:cluster_id], references) 47 | { bricks: BrickPresenter.list(bricks) }.to_json 48 | end 49 | 50 | get '/clusters/:cluster_id/notifications' do 51 | notifications = Tendrl::Notification.all 52 | NotificationPresenter.list_by_integration_id(notifications, params[:cluster_id]).to_json 53 | end 54 | 55 | get '/clusters/:cluster_id/jobs' do 56 | begin 57 | jobs = Tendrl::Job.all 58 | rescue Etcd::KeyNotFound 59 | jobs = [] 60 | end 61 | { jobs: JobPresenter.list_by_integration_id(jobs, params[:cluster_id]) }.to_json 62 | end 63 | 64 | post '/clusters/:cluster_id/import' do 65 | Tendrl.load_node_definitions 66 | Tendrl::Cluster.exist? params[:cluster_id] 67 | flow = Tendrl::Flow.new('namespace.tendrl', 'ImportCluster') 68 | body = JSON.parse(request.body.read) 69 | body['Cluster.volume_profiling_flag'] = if ['enable', 'disable'].include?(body['Cluster.volume_profiling_flag']) 70 | body['Cluster.volume_profiling_flag'] 71 | else 72 | 'leave-as-is' 73 | end 74 | job = Tendrl::Job.new( 75 | current_user, 76 | flow, 77 | integration_id: params[:cluster_id]).create(body) 78 | status 202 79 | { job_id: job.job_id }.to_json 80 | end 81 | 82 | post '/clusters/:cluster_id/unmanage' do 83 | Tendrl.load_node_definitions 84 | flow = Tendrl::Flow.new('namespace.tendrl', 'UnmanageCluster') 85 | body = JSON.parse(request.body.string.present? ? request.body.string : '{}') 86 | job = Tendrl::Job.new( 87 | current_user, 88 | flow, 89 | integration_id: params[:cluster_id]).create(body) 90 | status 202 91 | { job_id: job.job_id }.to_json 92 | end 93 | 94 | post '/clusters/:cluster_id/expand' do 95 | Tendrl.load_node_definitions 96 | flow = Tendrl::Flow.new 'namespace.tendrl', 'ExpandClusterWithDetectedPeers' 97 | job = Tendrl::Job.new( 98 | current_user, 99 | flow, 100 | integration_id: params[:cluster_id] 101 | ).create({}) 102 | status 202 103 | { job_id: job.job_id }.to_json 104 | end 105 | 106 | post '/clusters/:cluster_id/profiling' do 107 | Tendrl.load_definitions(params[:cluster_id]) 108 | body = JSON.parse(request.body.read) 109 | volume_profiling_flag = if ['enable', 'disable'].include?(body['Cluster.volume_profiling_flag']) 110 | body['Cluster.volume_profiling_flag'] 111 | else 112 | 'leave-as-is' 113 | end 114 | flow = Tendrl::Flow.new('namespace.gluster', 'EnableDisableVolumeProfiling') 115 | 116 | job = Tendrl::Job.new( 117 | current_user, 118 | flow, 119 | integration_id: params[:cluster_id], 120 | type: 'sds' 121 | ).create('Cluster.volume_profiling_flag' => volume_profiling_flag) 122 | status 202 123 | { job_id: job.job_id }.to_json 124 | end 125 | 126 | post '/clusters/:cluster_id/volumes/:volume_id/start_profiling' do 127 | Tendrl.load_definitions(params[:cluster_id]) 128 | flow = Tendrl::Flow.new('namespace.gluster', 'StartProfiling', 'Volume') 129 | job = Tendrl::Job.new( 130 | current_user, 131 | flow, 132 | integration_id: params[:cluster_id], 133 | type: 'sds' 134 | ).create('Volume.vol_id' => params[:volume_id]) 135 | status 202 136 | { job_id: job.job_id }.to_json 137 | end 138 | 139 | post '/clusters/:cluster_id/volumes/:volume_id/stop_profiling' do 140 | Tendrl.load_definitions(params[:cluster_id]) 141 | flow = Tendrl::Flow.new('namespace.gluster', 'StopProfiling', 'Volume') 142 | job = Tendrl::Job.new( 143 | current_user, 144 | flow, 145 | integration_id: params[:cluster_id], 146 | type: 'sds' 147 | ).create('Volume.vol_id' => params[:volume_id]) 148 | status 202 149 | { job_id: job.job_id }.to_json 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /app/controllers/jobs_controller.rb: -------------------------------------------------------------------------------- 1 | class JobsController < AuthenticatedUsersController 2 | get '/jobs' do 3 | begin 4 | jobs = Tendrl::Job.all 5 | rescue Etcd::KeyNotFound 6 | jobs = [] 7 | end 8 | { jobs: JobPresenter.list(jobs) }.to_json 9 | end 10 | 11 | before '/jobs/:job_id/?*?' do 12 | begin 13 | @job = Tendrl::Job.find(params[:job_id]) 14 | rescue Etcd::KeyNotFound => e 15 | e = Tendrl::HttpResponseErrorHandler.new( 16 | e, cause: '/jobs/id', object_id: params[:job_id] 17 | ) 18 | halt e.status, e.body.to_json 19 | end 20 | end 21 | 22 | get '/jobs/:job_id' do 23 | JobPresenter.single(@job).to_json 24 | end 25 | 26 | get '/jobs/:job_id/messages' do 27 | parent_job_messages = Tendrl::Job.messages(params[:job_id]) 28 | child_job_ids = JSON.parse @job.fetch('children', '[]') 29 | child_job_messages = child_job_ids.map do |child_id| 30 | Tendrl::Job.messages child_id 31 | end 32 | jobs = (parent_job_messages + child_job_messages.flatten).sort do |a, b| 33 | Time.parse(a['timestamp']) <=> Time.parse(b['timestamp']) 34 | end 35 | jobs.to_json 36 | end 37 | 38 | get '/jobs/:job_id/output' do 39 | @job['output'].to_json 40 | end 41 | 42 | get '/jobs/:job_id/status' do 43 | { status: @job['status'] }.to_json 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/controllers/nodes_controller.rb: -------------------------------------------------------------------------------- 1 | class NodesController < AuthenticatedUsersController 2 | 3 | before do 4 | Tendrl.load_node_definitions 5 | end 6 | 7 | get '/nodes' do 8 | nodes = Tendrl::Node.all 9 | { nodes: NodePresenter.list(nodes) }.to_json 10 | end 11 | 12 | post '/ImportCluster' do 13 | flow = Tendrl::Flow.new('namespace.tendrl', 'ImportCluster') 14 | body = JSON.parse(request.body.read) 15 | 16 | # ImportCluster job structure: 17 | # 18 | # job = { 19 | # "integration_id": "9a4b84e0-17b3-4543-af9f-e42000c52bfc", 20 | # "run": "tendrl.node_agent.flows.import_cluster.ImportCluster", 21 | # "status": "new", 22 | # "type": "node", 23 | # "node_ids": ["3943fab1-9ed2-4eb6-8121-5a69499c4568"], 24 | # "parameters": { 25 | # "TendrlContext.integration_id": "6b4b84e0-17b3-4543-af9f-e42000c52bfc", 26 | # "Node[]": ["3943fab1-9ed2-4eb6-8121-5a69499c4568"], 27 | # "DetectedCluster.sds_pkg_name": "gluster" 28 | # } 29 | # } 30 | # 31 | # Values sent by the UI: 32 | # 33 | # { 34 | # cluster_id: "c221ccdb-51d6-4b57-9f10-bcf30c7fa351" 35 | # hosts: [ 36 | # { 37 | # name: "dhcp43-203.lab.eng.blr.redhat.com", 38 | # release: "ceph 10.2.5", 39 | # role: "Monitor" 40 | # } 41 | # ], 42 | # node_ids: ["3b6eb27f-3e83-4751-9d45-85a989ae2b25"], 43 | # sds_type: "ceph", 44 | # sds_name: "ceph 10.2.5" 45 | # sds_version: "10.2.5" 46 | # } 47 | 48 | # TODO: UI should be sending the parameters as defined in the flows, API 49 | # shouldn't be translating. 50 | 51 | missing_params = [] 52 | ['sds_type', 'node_ids'].each do |param| 53 | missing_params << param unless body[param] and not body[param].empty? 54 | end 55 | halt 422, { errors: { missing: missing_params } }.to_json unless missing_params.empty? 56 | 57 | node_ids = body['node_ids'] 58 | halt 422, { errors: { message: "'node_ids' must be an array with values" } }.to_json unless node_ids.kind_of?(Array) and not node_ids.empty? 59 | detected_cluster_id = detected_cluster_id(node_ids.first) 60 | halt 422, { errors: { message: "Node #{node_ids.first} not found" } }.to_json if detected_cluster_id.nil? 61 | 62 | body['DetectedCluster.detected_cluster_id'] = detected_cluster_id 63 | body['DetectedCluster.sds_pkg_name'] = body['sds_type'] 64 | body['Node[]'] = node_ids 65 | job = Tendrl::Job.new(current_user, flow).create(body) 66 | 67 | status 202 68 | { job_id: job.job_id }.to_json 69 | end 70 | 71 | post '/CreateCluster' do 72 | flow = Tendrl::Flow.new('namespace.tendrl', 'CreateCluster') 73 | body = JSON.parse(request.body.read) 74 | 75 | # Ceph CreateCluster example 76 | # 77 | # { 78 | # "sds_name": "ceph", 79 | # "sds_version": "10.2.5", 80 | # "sds_parameters": { 81 | # "name": "MyCluster", 82 | # "cluster_id": "140cd3d5-58e4-4935-a954-d946ceff371d", 83 | # "public_network": "192.168.128.0/24", 84 | # "cluster_network": "192.168.220.0/24", 85 | # "conf_overrides": { 86 | # "global": { 87 | # "osd_pool_default_pg_num": 128, 88 | # "pool_default_pgp_num": 1 89 | # } 90 | # } 91 | # }, 92 | # "node_identifier": "ip", 93 | # "node_configuration": { 94 | # "10.0.0.24": { 95 | # "role": "ceph/mon", 96 | # "provisioning_ip": "10.0.0.24", 97 | # "monitor_interface": "eth0" 98 | # }, 99 | # "10.0.0.29": { 100 | # "role": "ceph/osd", 101 | # "provisioning_ip": "10.0.0.29", 102 | # "journal_size": 5192, 103 | # "journal_colocation": "false", 104 | # "storage_disks": [ 105 | # { 106 | # "device": "/dev/sda", 107 | # "journal": "/dev/sdc" 108 | # }, 109 | # { 110 | # "device": "/dev/sdb", 111 | # "journal": "/dev/sdc" 112 | # } 113 | # ] 114 | # }, 115 | # "10.0.0.30": { 116 | # "role": "ceph/osd", 117 | # "provisioning_ip": "10.0.0.30", 118 | # "journal_colocation": "true", 119 | # "storage_disks": [ 120 | # { 121 | # "device": "/dev/sda" 122 | # }, 123 | # { 124 | # "device": "/dev/sdb" 125 | # } 126 | # ] 127 | # } 128 | # } 129 | # } 130 | # 131 | # Job structure: 132 | # 133 | # { 134 | # "integration_id": "9a4b84e0-17b3-4543-af9f-e42000c52bfc", 135 | # "run": "tendrl.flows.CreateCluster", 136 | # "status": "new", 137 | # "type": "node", 138 | # "node_ids": [], 139 | # "tags": ["provisioner/ceph"], 140 | # "parameters": { 141 | # "TendrlContext.sds_name": "ceph", 142 | # "TendrlContext.sds_version": "10.2.5", 143 | # "TendrlContext.cluster_name": "MyCluster", 144 | # "TendrlContext.cluster_id": "9a4b84e0-17b3-4543-af9f-e42000c52bfc", 145 | # "Node[]": [ 146 | # "3a95fd96-876d-439a-a64d-70332c069aaa", 147 | # "3943fab1-9ed2-4eb6-8121-5a69499c4568", 148 | # "b10e00e9-e444-41c2-9517-df2118b42731" 149 | # ], 150 | # "Cluster.public_network": "192.168.128.0/24", 151 | # "Cluster.cluster_network": "192.168.220.0/24", 152 | # "Cluster.conf_overrides": { 153 | # "global": { 154 | # "osd_pool_default_pg_num": 128, 155 | # "pool_default_pgp_num": 1 156 | # } 157 | # }, 158 | # "Cluster.node_configuration": { 159 | # "3a95fd96-876d-439a-a64d-70332c069aaa": { 160 | # "role": "ceph/mon", 161 | # "provisioning_ip": "10.0.0.24", 162 | # "monitor_interface": "eth0" 163 | # }, 164 | # "3943fab1-9ed2-4eb6-8121-5a69499c4568": { 165 | # "role": "ceph/osd", 166 | # "provisioning_ip": "10.0.0.29", 167 | # "journal_size": 5192, 168 | # "journal_colocation": "false", 169 | # "storage_disks": [ 170 | # { 171 | # "device": "/dev/sda", 172 | # "journal": "/dev/sdc" 173 | # }, 174 | # { 175 | # "device": "/dev/sdb", 176 | # "journal": "/dev/sdc" 177 | # } 178 | # ] 179 | # }, 180 | # "b10e00e9-e444-41c2-9517-df2118b42731": { 181 | # "role": "ceph/osd", 182 | # "provisioning_ip": "10.0.0.30", 183 | # "journal_colocation": "true", 184 | # "storage_disks": [ 185 | # { 186 | # "device": "/dev/sda" 187 | # }, 188 | # { 189 | # "device": "/dev/sdb" 190 | # } 191 | # ] 192 | # } 193 | # } 194 | # } 195 | # } 196 | 197 | missing_params = [] 198 | ['sds_name', 'node_configuration'].each do |param| 199 | missing_params << param unless body[param] and not body[param].empty? 200 | end 201 | halt 422, { errors: { missing: missing_params } }.to_json unless missing_params.empty? 202 | 203 | node_identifier = body['node_identifier'] 204 | halt 422, { errors: { invalid: "'node_identifier', if specified, must be either 'uuid' or 'ip', provided: '#{node_identifier}'." } }.to_json \ 205 | if node_identifier and \ 206 | not ['uuid','ip'].include? node_identifier 207 | 208 | nodes = {} 209 | node_ids = body['node_configuration'].keys 210 | 211 | unavailable_nodes = [] 212 | node_ids.each do |node_id| 213 | node = case node_identifier 214 | when 'ip' 215 | Tendrl::Node.find_by_ip(node_id) 216 | when 'uuid' 217 | Tendrl::Node.new(uuid) 218 | end 219 | 220 | if node.nil? or not node.exist? 221 | unavailable_nodes << node_id 222 | next 223 | end 224 | 225 | nodes[node.uuid] = body['node_configuration'][node_id] 226 | end 227 | 228 | halt 422, { errors: { missing: "Unavailable nodes: #{unavailable_nodes.join(', ')}." } }.to_json unless unavailable_nodes.empty? 229 | 230 | parameters = {} 231 | ['sds_name', 'sds_version'].each do |param| 232 | parameters["TendrlContext.#{param}"] = body[param] 233 | end 234 | parameters['TendrlContext.cluster_name'] = body['sds_parameters']['name'] 235 | parameters['TendrlContext.cluster_id'] = body['sds_parameters']['cluster_id'] 236 | 237 | parameters['Node[]'] = nodes.keys 238 | 239 | ['public_network', 'cluster_network', 'conf_overrides'].each do |param| 240 | if body['sds_parameters']["#{param}"].present? 241 | parameters["Cluster.#{param}"] = body['sds_parameters']["#{param}"] 242 | end 243 | end 244 | parameters['Cluster.node_configuration'] = nodes 245 | 246 | job = Tendrl::Job.new(current_user, flow).create(parameters) 247 | status 202 248 | { job_id: job.job_id }.to_json 249 | end 250 | 251 | # Sample job structure 252 | 253 | # { 254 | # "run": "tendrl.flows.ExpandCluster", 255 | # "type": "node", 256 | # "created_from": "API", 257 | # "created_at": "2017-03-09T14:15:14Z", 258 | # "username": "admin", 259 | # "parameters": { 260 | # "Node[]": ["867d6aae-fb98-4060-9f6a-1da4e4988db8"], 261 | # "Cluster.node_configuration": { 262 | # "867d6aae-fb98-4060-9f6a-1da4e4988db8": { 263 | # "role": "glusterfs/node", 264 | # "provisioning_ip": "10.70.43.222", 265 | # }, 266 | # }, 267 | # "TendrlContext.sds_name": "gluster", 268 | # "TendrlContext.integration_id": "d3c644f1-0f94-43e1-946f-e40c4694d703" 269 | # }, 270 | # "tags":["provisioner/d3c644f1-0f94-43e1-946f-e40c4694d703"], 271 | # } 272 | 273 | put '/:cluster_id/ExpandCluster' do 274 | flow = Tendrl::Flow.new('namespace.tendrl', 'ExpandCluster') 275 | halt 404 if flow.nil? 276 | body = JSON.parse(request.body.read) 277 | missing_params = [] 278 | 279 | ['sds_name', 'Cluster.node_configuration'].each do |param| 280 | missing_params << param unless body[param] and not body[param].empty? 281 | end 282 | halt 422, { errors: { missing: missing_params } }.to_json unless missing_params.empty? 283 | 284 | ['sds_name'].each do |param| 285 | body["TendrlContext.#{param}"] = body.delete('sds_name') 286 | end 287 | 288 | body['Node[]'] = body['Cluster.node_configuration'].keys 289 | 290 | job = Tendrl::Job.new( 291 | current_user, 292 | flow, 293 | integration_id: params[:cluster_id] 294 | ).create(body) 295 | 296 | status 202 297 | { job_id: job.job_id }.to_json 298 | end 299 | 300 | post '/:flow' do 301 | flow = Tendrl::Flow.find_by_external_name_and_type( 302 | params[:flow], 'node_agent' 303 | ) 304 | halt 404 if flow.nil? 305 | body = JSON.parse(request.body.read) 306 | job = Tendrl::Job.new(current_user, flow).create(body) 307 | 308 | status 202 309 | { job_id: job.job_id }.to_json 310 | end 311 | 312 | 313 | private 314 | 315 | def detected_cluster_id(node_id) 316 | Tendrl.etcd.get("/nodes/#{node_id}/DetectedCluster/detected_cluster_id").value 317 | rescue Etcd::KeyNotFound 318 | nil 319 | end 320 | 321 | end 322 | -------------------------------------------------------------------------------- /app/controllers/notifications_controller.rb: -------------------------------------------------------------------------------- 1 | class NotificationsController < AuthenticatedUsersController 2 | 3 | get '/notifications' do 4 | notifications = Tendrl::Notification.all 5 | NotificationPresenter.list(notifications).to_json 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/ping_controller.rb: -------------------------------------------------------------------------------- 1 | class PingController < ApplicationController 2 | 3 | get '/ping' do 4 | { 5 | status: 'Ok' 6 | }.to_json 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | 3 | post '/login' do 4 | body = request.body.read 5 | attributes = JSON.parse(body).symbolize_keys 6 | 7 | user = Tendrl::User.authenticate(attributes[:username], 8 | attributes[:password]) 9 | if user.present? 10 | { access_token: user.generate_token }.to_json 11 | else 12 | status 401 13 | { errors: { message: 'Incorrect username or password.' } }.to_json 14 | end 15 | end 16 | 17 | # TODO move to different controller under AuthenticatedUsersController 18 | delete '/logout' do 19 | user = Tendrl::User.authenticate_access_token(access_token) 20 | if user 21 | user.delete_token(access_token) 22 | {}.to_json 23 | else 24 | halt 401, { errors: { message: 'Unauthorized'} }.to_json 25 | end 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < AuthenticatedUsersController 2 | 3 | get '/users' do 4 | authorized? 5 | UserPresenter.list(Tendrl::User.all).to_json 6 | end 7 | 8 | get '/users/:username' do 9 | authorized? unless current_user.username == params[:username] 10 | user = Tendrl::User.find(params[:username]) 11 | not_found if user.nil? 12 | UserPresenter.single(user).to_json 13 | end 14 | 15 | post '/users' do 16 | authorized? 17 | user_form = Tendrl::UserForm.new( 18 | Tendrl::User.new, 19 | user_attributes 20 | ) 21 | if user_form.valid? 22 | user = Tendrl::User.save(user_form.attributes) 23 | status 201 24 | UserPresenter.single(user).to_json 25 | else 26 | status 422 27 | { errors: user_form.errors.messages }.to_json 28 | end 29 | end 30 | 31 | put '/users/:username' do 32 | authorized? unless current_user.username == params[:username] 33 | user = Tendrl::User.find(params[:username]) 34 | not_found if user.nil? 35 | user_form = Tendrl::UserForm.new(user, user_attributes) 36 | if user_form.valid? 37 | attributes = user_form.attributes 38 | user = Tendrl::User.save(attributes) 39 | UserPresenter.single(user).to_json 40 | else 41 | status 422 42 | { errors: user_form.errors.messages }.to_json 43 | end 44 | end 45 | 46 | delete '/users/:username' do 47 | authorized? 48 | user = Tendrl::User.find(params[:username]) 49 | not_found if user.nil? 50 | if user.admin? 51 | halt 403, { errors: { message: 'Forbidden' } }.to_json 52 | else 53 | user.delete 54 | {}.to_json 55 | end 56 | end 57 | 58 | private 59 | 60 | def not_found 61 | halt 404, { errors: { message: 'Not found.' }}.to_json 62 | end 63 | 64 | def authorized? 65 | halt 403, { errors: { message: 'Forbidden' } }.to_json unless admin_user? 66 | end 67 | 68 | def user_attributes 69 | body = request.body.read 70 | @user_attributes ||= JSON.parse(body).symbolize_keys.slice( 71 | :name, 72 | :email, 73 | :username, 74 | :role, 75 | :password, 76 | :email_notifications 77 | ) 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /app/forms/user_form.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class UserForm 3 | include ActiveModel::Validations 4 | 5 | attr_reader :user 6 | attr_accessor :name, :username, :email, :password, 7 | :role, :email_notifications 8 | 9 | validates :name, :username, presence: true, length: { minimum: 4, maximum: 20 } 10 | 11 | validates :password, length: { minimum: 9, maximum: 128 }, if: :password_required? 12 | 13 | validates :email, format: { 14 | with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 15 | } 16 | 17 | validates :email_notifications, inclusion: { in: [true, false, "true", "false"] } 18 | 19 | validates :role, inclusion: { in: Tendrl::User::ROLES } 20 | 21 | validate :uniqueness 22 | 23 | validates_each :username, :role do |record, attr, value| 24 | existing_value = record.user.public_send(attr) 25 | if existing_value.present? && value != existing_value 26 | record.errors.add(attr, 'cannot be changed') 27 | end 28 | end 29 | 30 | def initialize(user, params) 31 | @user = user 32 | @name = params[:name] || user.name 33 | @username = params[:username] || user.username 34 | @email = params[:email] || user.email 35 | @role = params[:role] || user.role 36 | @password = params.delete(:password) 37 | if @password 38 | @password_salt = BCrypt::Engine.generate_salt 39 | @password_hash = BCrypt::Engine.hash_secret( 40 | @password, @password_salt 41 | ) 42 | else 43 | @password_salt = user.password_salt 44 | @password_hash = user.password_hash 45 | end 46 | @email_notifications = if params[:email_notifications].nil? 47 | user.email_notifications 48 | else 49 | [true, 'true'].include? params[:email_notifications] 50 | end 51 | end 52 | 53 | def attributes 54 | { 55 | name: @name, 56 | username: @username, 57 | password_salt: @password_salt, 58 | password_hash: @password_hash, 59 | email: @email, 60 | role: @role, 61 | email_notifications: @email_notifications 62 | } 63 | end 64 | 65 | private 66 | 67 | def password_required? 68 | if @user.new_record? 69 | true 70 | else 71 | password.present? 72 | end 73 | end 74 | 75 | def uniqueness 76 | if @username.present? || @email.present? 77 | users = Tendrl::User.all 78 | usernames, emails = [], [] 79 | users.each do |user| 80 | usernames << user.username 81 | emails << user.email 82 | end 83 | 84 | if usernames.include?(@username) && @username != @user.username 85 | errors.add(:username, 'is taken') 86 | end 87 | 88 | if emails.include?(@email) && @email != @user.email 89 | errors.add(:email, 'is taken') 90 | end 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /app/models/alert.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Alert 3 | class << self 4 | def all(path='alerts') 5 | alerts = Tendrl.etcd.get("/alerting/#{path}", recursive: true) 6 | .children.map do |alert| 7 | alert = Tendrl.recurse(alert).values.first 8 | alert['tags'] = JSON.parse(alert['tags']) || [] 9 | alert 10 | end 11 | alerts.sort do |a, b| 12 | Time.parse(a['time_stamp']) <=> Time.parse(b['time_stamp']) 13 | end 14 | end 15 | 16 | def find(alert_id) 17 | alert = Tendrl.recurse( 18 | Tendrl.etcd.get("/alerting/alerts/#{alert_id}") 19 | )[alert_id] 20 | alert['tags'] = JSON.parse(alert['tags']) rescue [] 21 | alert 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/models/brick.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Brick 3 | class << self 4 | def find_all_by_cluster_id_and_node_fqdn(cluster_id, fqdn) 5 | begin 6 | Tendrl.etcd.get("/clusters/#{cluster_id}/Bricks/all/#{fqdn}", recursive: true) 7 | .children.inject([]) do |bricks, node| 8 | brick = Tendrl.recurse node 9 | bricks + brick.values 10 | end 11 | rescue Etcd::KeyNotFound, Etcd::NotDir 12 | {} 13 | end 14 | end 15 | 16 | def find_refs_by_cluster_id_and_volume_id(cluster_id, volume_id) 17 | begin 18 | Tendrl.etcd.get("/clusters/#{cluster_id}/Volumes/#{volume_id}/Bricks", recursive: true) 19 | .children.inject({}) do |subvolume_refs, subvolume| 20 | sv_paths = Tendrl.recurse(subvolume, {}, downcase_keys: false) 21 | sv_paths.each { |name, paths| sv_paths[name] = paths.keys } 22 | subvolume_refs.merge sv_paths 23 | end 24 | rescue Etcd::KeyNotFound, Etcd::NotDir 25 | end 26 | end 27 | 28 | def find_by_cluster_id_and_refs(cluster_id, sub_volumes) 29 | sub_volumes.collect_concat do |sub_volume, paths| 30 | paths.collect_concat do |path| 31 | path = path.sub(':_', '/') 32 | begin 33 | path_brick = Tendrl.recurse( 34 | Tendrl.etcd.get( 35 | "/clusters/#{cluster_id}/Bricks/all/#{path}", 36 | recursive: true 37 | ) 38 | ) 39 | path_brick.each_value do |attrs| 40 | attrs['subvolume'] = sub_volume 41 | end 42 | path_brick.values 43 | rescue Etcd::KeyNotFound, Etcd::NotDir 44 | [] 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/models/cluster.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Cluster 3 | 4 | attr_accessor :attributes 5 | 6 | def initialize(attributes={}) 7 | @attributes = attributes 8 | end 9 | 10 | class << self 11 | def exist?(cluster_id) 12 | Tendrl.etcd.get "/clusters/#{cluster_id}" 13 | rescue Etcd::KeyNotFound => e 14 | raise Tendrl::HttpResponseErrorHandler.new( 15 | e, cause: '/clusters/id', object_id: cluster_id 16 | ) 17 | end 18 | 19 | def find(cluster_id) 20 | attributes = {} 21 | begin 22 | attributes = Tendrl.recurse( 23 | Tendrl.etcd.get( 24 | "/clusters/#{cluster_id}", recursive: true 25 | ) 26 | )[cluster_id] 27 | rescue Etcd::KeyNotFound 28 | raise Tendrl::HttpResponseErrorHandler.new( 29 | e, cause: '/clusters/id', object_id: cluster_id 30 | ) 31 | end 32 | new(attributes) 33 | end 34 | 35 | def all 36 | begin 37 | Tendrl.etcd.get('/clusters', recursive: true).children.map do |cluster| 38 | Tendrl.recurse(cluster) 39 | end 40 | rescue Etcd::KeyNotFound 41 | [] 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/models/job.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Job 3 | 4 | attr_reader :job_id, :payload 5 | 6 | def initialize(user, flow, options={}) 7 | @user = user 8 | @flow = flow 9 | @job_id = SecureRandom.uuid 10 | @type = options[:type] || 'node' 11 | @integration_id = options[:integration_id] || SecureRandom.uuid 12 | end 13 | 14 | def default_payload 15 | { 16 | status: 'new', 17 | name: @flow.flow_name, 18 | run: @flow.run, 19 | type: @type, 20 | created_from: 'API', 21 | created_at: Time.now.utc.iso8601, 22 | username: @user.username 23 | } 24 | end 25 | 26 | def create(parameters, routing = {}) 27 | parameters['TendrlContext.integration_id'] = @integration_id 28 | @payload = default_payload.merge parameters: parameters 29 | @payload[:node_ids] = routing[:node_ids] if routing[:node_ids].present? 30 | @payload[:tags] = @flow.tags(parameters) 31 | data = { 32 | job_id: @job_id, 33 | status: 'new', 34 | payload: @payload 35 | } 36 | Tendrl.etcd.set("/queue/#{@job_id}/data", value: data.to_json) 37 | self 38 | end 39 | 40 | class << self 41 | def all 42 | Tendrl.etcd.get('/queue', recursive: true).children.map do |job| 43 | job = Tendrl.recurse(job) 44 | job.values.first.merge('job_id' => job.keys.first) 45 | end 46 | end 47 | 48 | def find(job_id) 49 | Tendrl.recurse( 50 | Tendrl.etcd.get("/queue/#{job_id}", recursive: true) 51 | )[job_id].merge('job_id' => job_id) 52 | end 53 | 54 | def messages(job_id) 55 | messages = [] 56 | begin 57 | Tendrl.etcd.get("/messages/jobs/#{job_id}", recursive: true) 58 | .children.each do |child| 59 | begin 60 | if child.value.present? 61 | messages << JSON.parse(child.value) 62 | end 63 | rescue JSON::ParserError 64 | end 65 | end 66 | rescue Etcd::KeyNotFound 67 | end 68 | messages 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /app/models/node.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Node 3 | 4 | class << self 5 | 6 | # TODO: The interface sucks. Don't like making people use .nil? here and 7 | # .exist? in the actual object. 8 | def self.find_by_ip(ip) 9 | begin 10 | uuid = Tendrl.etcd.get("/indexes/ip/#{ip}").value 11 | rescue ::Etcd::KeyNotFound 12 | return nil 13 | end 14 | new(uuid) 15 | end 16 | 17 | def all 18 | Tendrl.etcd.get('/nodes').children.map do |node| 19 | begin 20 | nodecontext = Tendrl.recurse(Tendrl.etcd.get("#{node.key}/NodeContext")) 21 | tendrlcontext = Tendrl.recurse(Tendrl.etcd.get("#{node.key}/TendrlContext")) 22 | counters = Tendrl.recurse(Tendrl.etcd.get("#{node.key}/alert_counters")) 23 | node_key = node.key.split('/')[-1] 24 | { node_key => nodecontext.merge(tendrlcontext).merge(counters) } 25 | rescue Etcd::KeyNotFound, Etcd::NotDir 26 | end 27 | end.compact 28 | rescue Etcd::KeyNotFound, Etcd::NotDir 29 | [] 30 | end 31 | 32 | def find_all_by_cluster_id(cluster_id) 33 | begin 34 | tendrlcontext = Tendrl.recurse(Tendrl.etcd.get("/clusters/#{cluster_id}/TendrlContext")) 35 | Tendrl.etcd.get("/clusters/#{cluster_id}/nodes", recursive: true) 36 | .children.map do |node| 37 | node = Tendrl.recurse(node) 38 | node.values.first.merge!(tendrlcontext) 39 | node 40 | end 41 | rescue Etcd::KeyNotFound, Etcd::NotDir 42 | [] 43 | end 44 | end 45 | 46 | def find_by_cluster_id(cluster_id, node_id) 47 | Tendrl.recurse(Tendrl.etcd.get("/clusters/#{cluster_id}/nodes/#{node_id}/NodeContext"))['nodecontext'] 48 | rescue Etcd::KeyNotFound 49 | end 50 | end 51 | 52 | attr_reader :uuid 53 | 54 | def initialize(uuid) 55 | @uuid = uuid 56 | @exists = true 57 | begin 58 | @path = Tendrl.etcd.get("/nodes/#{uuid}") 59 | rescue ::Etcd::KeyNotFound 60 | @exists = false 61 | end 62 | end 63 | 64 | def exist? 65 | @exists 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /app/models/notification.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Notification 3 | 4 | class << self 5 | 6 | def all 7 | Tendrl.etcd.get('/notifications', recursive: 8 | true).children.map do |children| 9 | Tendrl.recurse(children) 10 | end 11 | rescue Etcd::KeyNotFound, Etcd::NotDir 12 | [] 13 | end 14 | 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class User 3 | 4 | ADMIN = 'admin' 5 | NORMAL = 'normal' 6 | LIMITED = 'limited' 7 | 8 | ROLES = [ADMIN, NORMAL, LIMITED] 9 | 10 | attr_accessor :name, :email, :username, :role, :password_hash, 11 | :password_salt, :email_notifications 12 | 13 | def initialize(attributes={}) 14 | attributes = attributes.with_indifferent_access 15 | @name = attributes[:name] 16 | @email = attributes[:email] 17 | @username = attributes[:username] 18 | @email_notifications = attributes[:email_notifications] 19 | @role = attributes[:role] 20 | @password_hash = attributes[:password_hash] 21 | @password_salt = attributes[:password_salt] 22 | end 23 | 24 | def attributes 25 | { 26 | name: name, 27 | email: email, 28 | username: username, 29 | email_notifications: email_notifications, 30 | role: role 31 | } 32 | end 33 | 34 | def admin? 35 | @role == ADMIN 36 | end 37 | 38 | def normal? 39 | admin? || @role == NORMAL 40 | end 41 | 42 | def limited? 43 | @role == LIMITED 44 | end 45 | 46 | def generate_token 47 | token = SecureRandom.hex(32) 48 | Tendrl.etcd.set( 49 | "/_tendrl/access_tokens/#{token}", 50 | value: username, 51 | ttl: 604800 # 1.week 52 | ) 53 | token 54 | end 55 | 56 | def delete_token(access_token) 57 | Tendrl.etcd.delete("/_tendrl/access_tokens/#{access_token}") 58 | end 59 | 60 | def new_record? 61 | password_hash.blank? 62 | end 63 | 64 | def delete 65 | Tendrl.etcd.delete("/_tendrl/indexes/notifications/email_notifications/#{username}") rescue Etcd::KeyNotFound 66 | Tendrl.etcd.delete("/_tendrl/users/#{username}", recursive: true) 67 | end 68 | 69 | class << self 70 | def all 71 | Tendrl.etcd.get('/_tendrl/users', recursive: true) 72 | .children.collect_concat do |user| 73 | new(Tendrl.recurse(user).values.first) 74 | end 75 | end 76 | 77 | def find(username) 78 | attrs = Tendrl.recurse( 79 | Tendrl.etcd.get("/_tendrl/users/#{username}") 80 | )[username] 81 | user = new(attrs) 82 | user 83 | rescue Etcd::KeyNotFound 84 | nil 85 | end 86 | 87 | def find_user_by_access_token(token) 88 | username = Tendrl.etcd.get("/_tendrl/access_tokens/#{token}").value 89 | find(username) 90 | rescue Etcd::KeyNotFound 91 | nil 92 | end 93 | 94 | def save(attributes) 95 | user_path = "/_tendrl/users/#{attributes[:username]}" 96 | Tendrl.etcd.set "#{user_path}/data", value: attributes.to_json 97 | user = find(attributes[:username]) 98 | post_save_hooks(user) 99 | user 100 | end 101 | 102 | def post_save_hooks(user) 103 | update_email_notification_indexes(user) 104 | end 105 | 106 | def update_email_notification_indexes(user) 107 | email_index = "/_tendrl/indexes/notifications/email_notifications/#{user.username}" 108 | if user.email_notifications 109 | Tendrl.etcd.set(email_index, value: user.email) 110 | else 111 | Tendrl.etcd.delete(email_index) rescue Etcd::KeyNotFound 112 | end 113 | end 114 | 115 | def authenticate(username, password) 116 | user = find(username) 117 | if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt) 118 | user 119 | else 120 | nil 121 | end 122 | end 123 | 124 | def authenticate_access_token(access_token) 125 | if user = find_user_by_access_token(access_token) 126 | user 127 | else 128 | nil 129 | end 130 | rescue Etcd::KeyNotFound 131 | nil 132 | end 133 | 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /app/models/volume.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Volume 3 | class << self 4 | 5 | def find_all_by_cluster_id(cluster_id) 6 | begin 7 | Tendrl.etcd.get("/clusters/#{cluster_id}/Volumes", recursive: true) 8 | .children.map do |node| 9 | Tendrl.recurse(node) 10 | end 11 | rescue Etcd::KeyNotFound, Etcd::NotDir 12 | [] 13 | end 14 | end 15 | 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/presenters/brick_presenter.rb: -------------------------------------------------------------------------------- 1 | module BrickPresenter 2 | class << self 3 | def list(bricks) 4 | bricks.map do |attributes| 5 | %w[devices partitions pv].each do |key| 6 | attributes[key] = JSON.parse(attributes[key] || '[]') 7 | end 8 | attributes 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/presenters/cluster_presenter.rb: -------------------------------------------------------------------------------- 1 | module ClusterPresenter 2 | 3 | class << self 4 | 5 | def list(cluster_list) 6 | clusters = [] 7 | cluster_list.each do |cluster| 8 | clusters << single(cluster) 9 | end 10 | clusters.compact 11 | end 12 | 13 | def single(cluster_attributes) 14 | cluster = nil 15 | cluster_attributes.each do |cluster_id, attributes| 16 | context = attributes.delete('tendrlcontext') 17 | break if context.nil? 18 | context['cluster_id'] = cluster_id 19 | attributes.slice!( 20 | 'errors', 21 | 'globaldetails', 22 | 'short_name', 23 | 'nodes', 24 | 'public_network', 25 | 'cluster_network', 26 | 'is_managed', 27 | 'volume_profiling_state', 28 | 'alert_counters', 29 | 'status', 30 | 'current_job' 31 | ) 32 | attributes['errors'] = JSON.parse(attributes['errors']) rescue [] 33 | nodes = attributes.delete('nodes') 34 | cluster_nodes = [] 35 | if nodes.present? 36 | nodes.each do |node_id, values| 37 | next if values['nodecontext'].blank? 38 | values['nodecontext']['tags'] = JSON.parse(values['nodecontext']['tags']) rescue [] 39 | values['nodecontext']['status'] ||= 'DOWN' 40 | cluster_nodes << values['nodecontext'].merge({ node_id: node_id }) 41 | end 42 | end 43 | 44 | if bricks = attributes.delete('bricks') 45 | attributes.merge!(bricks: bricks(bricks)) 46 | end 47 | 48 | cluster = context.merge(attributes).merge(nodes: cluster_nodes) 49 | end 50 | cluster 51 | end 52 | 53 | def bricks(bricks) 54 | all = bricks.delete('all') || {} 55 | free = {} 56 | used = {} 57 | if all.present? 58 | all.each do |device, attributes| 59 | if bricks['free'].present? && bricks['free'].keys.include?(device) 60 | free[device] = attributes 61 | elsif bricks['used'].present? && bricks['used'].keys.include?(device) 62 | used[device] = attributes 63 | end 64 | end 65 | end 66 | { all: all, used: used, free: free } 67 | end 68 | 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/presenters/job_presenter.rb: -------------------------------------------------------------------------------- 1 | module JobPresenter 2 | class << self 3 | def single(job) 4 | payload = job['payload'] 5 | return if job['payload'].blank? || !['API', 'BackendFlow'].include?(payload['created_from']) 6 | { 7 | job_id: job['job_id'], 8 | status: job['status'], 9 | flow: payload['name'] || (payload['run'] && payload['run'].split('.').last), 10 | parameters: payload['parameters'], 11 | created_at: payload['created_at'], 12 | updated_at: job['updated_at'], 13 | status_url: "/jobs/#{job['job_id']}/status", 14 | messages_url: "/jobs/#{job['job_id']}/messages", 15 | output_url: "/jobs/#{job['job_id']}/output", 16 | errors: job['errors'] 17 | } 18 | end 19 | 20 | def list(jobs) 21 | jobs.map do |job| 22 | single(job) 23 | end.compact 24 | end 25 | 26 | def list_by_integration_id(jobs, integration_id) 27 | jobs.map do |job| 28 | next if job['payload'].blank? 29 | if job['payload']['parameters']['TendrlContext.integration_id'] == integration_id 30 | single(job) 31 | end 32 | end.compact 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/presenters/node_presenter.rb: -------------------------------------------------------------------------------- 1 | module NodePresenter 2 | class << self 3 | def list(nodes_list) 4 | nodes = [] 5 | nodes_list.each do |node| 6 | node.each do |_, attributes| 7 | attributes.slice!('nodecontext','tendrlcontext','alert_counters') 8 | node_attr = attributes.delete('nodecontext') 9 | next if node_attr.blank? 10 | node_attr['tags'] = JSON.parse(node_attr['tags']) rescue [] 11 | node_attr['status'] ||= 'DOWN' 12 | if cluster = attributes.delete('tendrlcontext') 13 | cluster.delete('node_id') 14 | end 15 | nodes << node_attr.merge(attributes).merge(cluster: (cluster || {})) 16 | end 17 | end 18 | nodes 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/presenters/notification_presenter.rb: -------------------------------------------------------------------------------- 1 | module NotificationPresenter 2 | 3 | class << self 4 | 5 | def list(raw_notifications) 6 | notifications = [] 7 | raw_notifications.each do |notification| 8 | notification.each do |notification_id, attributes| 9 | attributes.slice!('message_id', 'timestamp', 'priority', 'payload') 10 | payload = attributes.delete('payload') 11 | message = JSON.parse(payload['message'])['tags']['message'] rescue "" 12 | attributes['message_id'] = notification_id 13 | attributes['message'] = message 14 | notifications << attributes 15 | end 16 | end 17 | notifications.sort do |a,b| 18 | Time.parse(b['timestamp']) <=> Time.parse(a['timestamp']) 19 | end 20 | end 21 | 22 | def list_by_integration_id(raw_notifications, integration_id) 23 | notifications = [] 24 | raw_notifications.each do |notification| 25 | notification.each do |notification_id, attributes| 26 | next unless attributes['integration_id'] == integration_id 27 | attributes.slice!('message_id', 'timestamp', 'priority', 'payload') 28 | payload = attributes.delete('payload') 29 | message = JSON.parse(payload['message'])['tags']['message'] rescue "" 30 | attributes['message_id'] = notification_id 31 | attributes['message'] = message 32 | notifications << attributes 33 | end 34 | end 35 | notifications.sort do |a,b| 36 | Time.parse(b['timestamp']) <=> Time.parse(a['timestamp']) 37 | end 38 | end 39 | 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /app/presenters/user_presenter.rb: -------------------------------------------------------------------------------- 1 | module UserPresenter 2 | class << self 3 | def single(user) 4 | { 5 | email: user.email, 6 | username: user.username, 7 | name: user.name, 8 | role: user.role, 9 | email_notifications: user.email_notifications 10 | } 11 | end 12 | 13 | def list(users) 14 | users.map do |user| 15 | single(user) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/presenters/volume_presenter.rb: -------------------------------------------------------------------------------- 1 | module VolumePresenter 2 | class << self 3 | def list(raw_volumes) 4 | volumes = [] 5 | raw_volumes.each do |volume| 6 | volume.each do |vol_id, attributes| 7 | attributes['vol_id'] = vol_id 8 | attributes.delete('bricks') 9 | attributes.delete('options') 10 | volumes << attributes 11 | end 12 | end 13 | volumes 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path("../lib", __FILE__)) 2 | 3 | require 'tendrl' 4 | 5 | if ENV['ENABLE_PROFILING'] 6 | if ENV['RACK_ENV'] == 'production' 7 | path = '/var/lib/tendrl/profiling/api' 8 | else 9 | path = './tmp/profiling' 10 | end 11 | require 'ruby-prof' 12 | use Rack::RubyProf, path: path 13 | end 14 | 15 | map('/1.0') { 16 | use PingController 17 | use SessionsController 18 | use AlertingController 19 | use NotificationsController 20 | use JobsController 21 | use UsersController 22 | use NodesController 23 | use ClustersController 24 | use AuthenticatedUsersController 25 | run ApplicationController 26 | } 27 | -------------------------------------------------------------------------------- /config/apache.vhost-ssl.sample: -------------------------------------------------------------------------------- 1 | # vim: ft=apache 2 | ## Sample apache configuration file for tendrl over https. Replace the 3 | ## placeholder in the VirtualHost tag with the correct IP. 4 | 5 | ## This configuration file assumes that mod_ssl package is installed on 6 | ## RHEL/CentOS hosts and it's configuration has not been modified. 7 | 8 | 9 | # Adjust the ServerName directive to reflect the FQDN for your host. Add 10 | # additional ServerAlias directives if required. 11 | ServerName tendrl 12 | 13 | # This is where the tendrl frontend assets will be installed. 14 | DocumentRoot /var/www/tendrl 15 | 16 | # SSL Engine Switch: 17 | # Enable/Disable SSL for this virtual host. 18 | SSLEngine on 19 | 20 | # SSL Protocol support: 21 | # List the enable protocol levels with which clients will be able to 22 | # connect. Disable SSLv2 access by default: 23 | SSLProtocol all -SSLv2 24 | 25 | # SSL Cipher Suite: 26 | # List the ciphers that the client is permitted to negotiate. 27 | # See the mod_ssl documentation for a complete list. 28 | SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5:!SEED:!IDEA 29 | 30 | # Server Certificate: 31 | # Point SSLCertificateFile at a PEM encoded certificate. If 32 | # the certificate is encrypted, then you will be prompted for a 33 | # pass phrase. Note that a kill -HUP will prompt again. A new 34 | # certificate can be generated using the genkey(1) command. 35 | SSLCertificateFile /etc/pki/tls/certs/localhost.crt 36 | 37 | # Server Private Key: 38 | # If the key is not combined with the certificate, use this 39 | # directive to point at the key file. Keep in mind that if 40 | # you've both a RSA and a DSA private key you can configure 41 | # both in parallel (to also allow the use of DSA ciphers, etc.) 42 | SSLCertificateKeyFile /etc/pki/tls/private/localhost.key 43 | 44 | # Adjust the logging configuration as required. 45 | ErrorLog "logs/tendrl-error_log" 46 | 47 | # Per-Server Logging: 48 | # The home of a custom SSL log file. Use this when you want a 49 | # compact non-error SSL logfile on a virtual host basis. 50 | CustomLog logs/tendrl-ssl-access_log \ 51 | "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" 52 | 53 | # The requests to /api/ will all be forwarded to the tendrl-api application. 54 | ProxyPass "/api" http://127.0.0.1:9292/ 55 | ProxyPassReverse "/api" http://127.0.0.1:9292/ 56 | 57 | # https request to /grafana will be redirected to grafana server as http request 58 | ProxyPass /grafana http://127.0.0.1:3000 59 | ProxyPassReverse /grafana http://127.0.0.1:3000/grafana 60 | 61 | -------------------------------------------------------------------------------- /config/apache.vhost.sample: -------------------------------------------------------------------------------- 1 | # vim: ft=apache 2 | ## Sample apache configuration file for tendrl. 3 | 4 | 5 | # Adjust the ServerName directive to reflect the FQDN for your host. Add 6 | # additional ServerAlias directives if required. 7 | ServerName tendrl 8 | 9 | # This is where the tendrl frontend assets will be installed. 10 | DocumentRoot /var/www/tendrl 11 | 12 | # Adjust the logging configuration as required. 13 | ErrorLog "logs/tendrl-error_log" 14 | CustomLog "logs/tendrl-access_log" combined 15 | 16 | # The requests to /api/ will all be forwarded to the tendrl-api application. 17 | ProxyPass "/api" http://127.0.0.1:9292/ 18 | ProxyPassReverse "/api" http://127.0.0.1:9292/ 19 | 20 | # When SSL is enabled, the following rule will redirect all http requests to 21 | # https. Ensure that the same IP used by the SSL VirtualHost is used. 22 | # 23 | # IMPORTANT: Be sure to comment out the DocumentRoot, ProxyPass and 24 | # ProxyPassReverse directives above. 25 | #Redirect permanent / https://%ssl_virtualhost_fqdn%/ 26 | 27 | # The requests to /grafana will be forwarded to grafana server 28 | ProxyPass /grafana http://127.0.0.1:3000 29 | ProxyPassReverse /grafana http://127.0.0.1:3000/grafana 30 | 31 | -------------------------------------------------------------------------------- /config/etcd.sample.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :development: 3 | :host: '127.0.0.1' 4 | :port: 2379 5 | :ca_cert_file: '' 6 | :client_cert_file: '' 7 | :client_key_file: '' 8 | :passphrase: '' 9 | 10 | :production: 11 | :host: '127.0.0.1' 12 | :port: 2379 13 | :ca_cert_file: '' 14 | :client_cert_file: '' 15 | :client_key_file: '' 16 | :passphrase: '' 17 | 18 | 19 | -------------------------------------------------------------------------------- /config/initializers/etcd.rb: -------------------------------------------------------------------------------- 1 | config = Tendrl.load_config(ENV['RACK_ENV']).symbolize_keys 2 | etcd_config = { host: config[:host], port: config[:port] } 3 | 4 | if [config[:ca_cert_file], config[:client_cert_file], config[:client_key_file]] 5 | .all?{|c| c.present? } 6 | etcd_config.merge!(Tendrl.load_cert_config(config)) 7 | end 8 | 9 | Tendrl::ETCD_CACHE = {} 10 | 11 | module Tendrl 12 | module EtcdCache 13 | def cache 14 | Tendrl::ETCD_CACHE 15 | end 16 | 17 | def cached_get(path, opts = {}) 18 | if cache[path].present? && cache[path]['updated'] > 5.minutes.ago 19 | return cache[path]['data'] 20 | end 21 | cache[path] = { 22 | 'data' => get(path, opts), 23 | 'updated' => Time.now 24 | } 25 | cache[path]['data'] 26 | end 27 | 28 | def cached_delete(path, opts = {}) 29 | cache.delete path 30 | delete path, opts 31 | end 32 | end 33 | end 34 | 35 | module Etcd 36 | class Client 37 | include Tendrl::EtcdCache 38 | end 39 | end 40 | 41 | Tendrl.etcd = Etcd.client(etcd_config) 42 | -------------------------------------------------------------------------------- /config/puma/production.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env puma 2 | 3 | app_dir = '/usr/share/tendrl-api' 4 | 5 | directory app_dir 6 | 7 | environment 'production' 8 | 9 | stdout_redirect "#{app_dir}/log/puma.log", "#{app_dir}/log/error.log", true 10 | 11 | threads_count = Integer(ENV['PUMA_MAX_THREADS'] || 2) 12 | threads threads_count, threads_count 13 | 14 | bind 'tcp://127.0.0.1:9292' 15 | 16 | # === Cluster mode === 17 | 18 | workers Integer(ENV['PUMA_WORKERS'] || 4) 19 | 20 | preload_app! 21 | 22 | tag 'Tendrl API' 23 | 24 | worker_timeout 60 25 | 26 | -------------------------------------------------------------------------------- /config/tendrl-api_logrotate.conf: -------------------------------------------------------------------------------- 1 | /var/log/tendrl/api/*.log 2 | { 3 | su tendrl-api tendrl-api 4 | size=100M 5 | rotate 10 6 | missingok 7 | compress 8 | notifempty 9 | copytruncate 10 | } 11 | -------------------------------------------------------------------------------- /config/tendrl.sample.yml: -------------------------------------------------------------------------------- 1 | --- 2 | config_version: '1.0' 3 | sdss_type: ceph 4 | objects: 5 | cluster: 6 | properties: 7 | cluster_id: String 8 | name: String 9 | config: Hash 10 | health: Hash 11 | osd: 12 | properties: 13 | osd_id: String 14 | osd_cluster_id: String 15 | reweight: Float 16 | server: %s 17 | states: 18 | up: Boolean 19 | in: Boolean 20 | actions: Array 21 | pool: 22 | states: 23 | full: Boolean 24 | properties: 25 | pool_id: Integer 26 | name: String 27 | size: Integer 28 | pg_num: Integer 29 | crush_rule_set: %s 30 | min_size: Integer 31 | crash_replay_interval: Integer 32 | pgp_num: Integer 33 | hashpspool: Boolean 34 | quota_max_objects: Integer 35 | quota_max_bytes: Integer 36 | relationships: 37 | cluster: 38 | has_many: 39 | - node 40 | - osd 41 | - pool 42 | - mon 43 | - service 44 | - plancement_group 45 | - crush_node 46 | - crush_type 47 | has_one: 48 | - crush_map 49 | - crush_rule_set 50 | -------------------------------------------------------------------------------- /dependencies/etcd.spec: -------------------------------------------------------------------------------- 1 | # Generated from etcd-0.3.0.gem by gem2rpm -*- rpm-spec -*- 2 | %global gem_name etcd 3 | 4 | Name: rubygem-%{gem_name} 5 | Version: 0.3.0 6 | Release: 1%{?dist} 7 | Summary: Ruby client library for etcd 8 | Group: Development/Languages 9 | License: MIT 10 | URL: https://github.com/ranjib/etcd-ruby 11 | Source0: https://rubygems.org/gems/%{gem_name}-%{version}.gem 12 | BuildRequires: ruby(release) 13 | BuildRequires: rubygems-devel 14 | BuildRequires: ruby >= 1.9 15 | # BuildRequires: rubygem(uuid) 16 | # BuildRequires: rubygem(rspec) 17 | BuildArch: noarch 18 | 19 | %description 20 | Ruby client library for etcd. 21 | 22 | 23 | %package doc 24 | Summary: Documentation for %{name} 25 | Group: Documentation 26 | Requires: %{name} = %{version}-%{release} 27 | BuildArch: noarch 28 | 29 | %description doc 30 | Documentation for %{name}. 31 | 32 | %prep 33 | gem unpack %{SOURCE0} 34 | 35 | %setup -q -D -T -n %{gem_name}-%{version} 36 | 37 | gem spec %{SOURCE0} -l --ruby > %{gem_name}.gemspec 38 | 39 | %build 40 | # Create the gem as gem install only works on a gem file 41 | gem build %{gem_name}.gemspec 42 | 43 | # %%gem_install compiles any C extensions and installs the gem into ./%%gem_dir 44 | # by default, so that we can move it into the buildroot in %%install 45 | %gem_install 46 | 47 | %install 48 | mkdir -p %{buildroot}%{gem_dir} 49 | cp -a .%{gem_dir}/* \ 50 | %{buildroot}%{gem_dir}/ 51 | 52 | 53 | 54 | 55 | # Run the test suite 56 | %check 57 | pushd .%{gem_instdir} 58 | 59 | popd 60 | 61 | %files 62 | %dir %{gem_instdir} 63 | %{gem_instdir}/.coco.yml 64 | %exclude %{gem_instdir}/.gitignore 65 | %{gem_instdir}/.rspec 66 | %exclude %{gem_instdir}/.rubocop.yml 67 | %exclude %{gem_instdir}/.travis.yml 68 | %{gem_instdir}/Guardfile 69 | %license %{gem_instdir}/LICENSE.txt 70 | %{gem_libdir} 71 | %exclude %{gem_cache} 72 | %{gem_spec} 73 | 74 | %files doc 75 | %doc %{gem_docdir} 76 | %{gem_instdir}/Gemfile 77 | %doc %{gem_instdir}/README.md 78 | %{gem_instdir}/Rakefile 79 | %{gem_instdir}/etcd.gemspec 80 | %{gem_instdir}/spec 81 | 82 | %changelog 83 | * Tue Nov 15 2016 Tim - 0.3.0-1 84 | - Initial package 85 | -------------------------------------------------------------------------------- /dependencies/puma.spec: -------------------------------------------------------------------------------- 1 | # Generated from puma-3.6.0.gem by gem2rpm -*- rpm-spec -*- 2 | %global gem_name puma 3 | 4 | Name: rubygem-%{gem_name} 5 | Version: 3.6.0 6 | Release: 1%{?dist} 7 | Summary: Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications 8 | Group: Development/Languages 9 | License: BSD-3-Clause 10 | URL: http://puma.io 11 | Source0: https://rubygems.org/gems/%{gem_name}-%{version}.gem 12 | BuildRequires: ruby(release) 13 | BuildRequires: rubygems-devel 14 | BuildRequires: ruby-devel >= 1.8.7 15 | 16 | # BuildRequires: rubygem(rack) >= 1.1 17 | # BuildRequires: rubygem(rack) < 2.0 18 | # BuildRequires: rubygem(rake-compiler) => 0.8 19 | # BuildRequires: rubygem(rake-compiler) < 1 20 | # BuildRequires: rubygem(hoe) => 3.15 21 | # BuildRequires: rubygem(hoe) < 4 22 | 23 | %description 24 | Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for 25 | Ruby/Rack applications. Puma is intended for use in both development and 26 | production environments. In order to get the best throughput, it is highly 27 | recommended that you use a Ruby implementation with real threads like 28 | Rubinius or JRuby. 29 | 30 | 31 | %package doc 32 | Summary: Documentation for %{name} 33 | Group: Documentation 34 | Requires: %{name} = %{version}-%{release} 35 | BuildArch: noarch 36 | 37 | %description doc 38 | Documentation for %{name}. 39 | 40 | %prep 41 | gem unpack %{SOURCE0} 42 | 43 | %setup -q -D -T -n %{gem_name}-%{version} 44 | 45 | gem spec %{SOURCE0} -l --ruby > %{gem_name}.gemspec 46 | 47 | %build 48 | # Create the gem as gem install only works on a gem file 49 | gem build %{gem_name}.gemspec 50 | 51 | # %%gem_install compiles any C extensions and installs the gem into ./%%gem_dir 52 | # by default, so that we can move it into the buildroot in %%install 53 | %gem_install 54 | 55 | %install 56 | mkdir -p %{buildroot}%{gem_dir} 57 | cp -a .%{gem_dir}/* \ 58 | %{buildroot}%{gem_dir}/ 59 | 60 | mkdir -p %{buildroot}%{gem_extdir_mri}/puma 61 | cp -a .%{gem_extdir_mri}/gem.build_complete %{buildroot}%{gem_extdir_mri}/ || : 62 | cp -a .%{gem_extdir_mri}/puma/*.so %{buildroot}%{gem_extdir_mri}/puma || : 63 | cp -a .%{gem_extdir_mri}/{gem.build_complete,*.so} %{buildroot}%{gem_extdir_mri}/ || : 64 | 65 | # Prevent dangling symlink in -debuginfo (rhbz#878863). 66 | rm -rf %{buildroot}%{gem_instdir}/ext/ 67 | 68 | mkdir -p %{buildroot}%{_bindir} 69 | cp -pa .%{_bindir}/* \ 70 | %{buildroot}%{_bindir}/ 71 | 72 | find %{buildroot}%{gem_instdir}/bin -type f | xargs chmod a+x 73 | 74 | # Run the test suite 75 | %check 76 | pushd .%{gem_instdir} 77 | 78 | popd 79 | 80 | %files 81 | %dir %{gem_instdir} 82 | %{_bindir}/puma 83 | %{_bindir}/pumactl 84 | %{gem_extdir_mri} 85 | %{gem_instdir}/DEPLOYMENT.md 86 | %license %{gem_instdir}/LICENSE 87 | %{gem_instdir}/Manifest.txt 88 | %{gem_instdir}/bin 89 | %{gem_libdir} 90 | %{gem_instdir}/tools 91 | %exclude %{gem_cache} 92 | %{gem_spec} 93 | 94 | %files doc 95 | %doc %{gem_docdir} 96 | %{gem_instdir}/Gemfile 97 | %doc %{gem_instdir}/History.txt 98 | %doc %{gem_instdir}/README.md 99 | %{gem_instdir}/Rakefile 100 | %doc %{gem_instdir}/docs 101 | %{gem_instdir}/puma.gemspec 102 | 103 | %changelog 104 | * Fri Nov 18 2016 Tim - 3.6.0-2 105 | - Fix .so file path 106 | - Fix ext not found error 107 | 108 | * Wed Nov 16 2016 Tim - 3.6.0-1 109 | - Initial package 110 | -------------------------------------------------------------------------------- /dependencies/rubygem-bundler.spec: -------------------------------------------------------------------------------- 1 | # Generated from bundler-1.13.6.gem by gem2rpm -*- rpm-spec -*- 2 | %global gem_name bundler 3 | 4 | Name: rubygem-%{gem_name} 5 | Version: 1.13.6 6 | Release: 1%{?dist} 7 | Summary: The best way to manage your application's dependencies 8 | Group: Development/Languages 9 | License: MIT 10 | URL: http://bundler.io 11 | Source0: https://rubygems.org/gems/%{gem_name}-%{version}.gem 12 | BuildRequires: ruby(release) 13 | BuildRequires: rubygems-devel >= 1.3.6 14 | BuildRequires: ruby >= 1.8.7 15 | # BuildRequires: rubygem(automatiek) => 0.1.0 16 | # BuildRequires: rubygem(automatiek) < 0.2 17 | # BuildRequires: rubygem(mustache) = 0.99.6 18 | # BuildRequires: rubygem(rdiscount) => 2.2 19 | # BuildRequires: rubygem(rdiscount) < 3 20 | # BuildRequires: rubygem(ronn) => 0.7.3 21 | # BuildRequires: rubygem(ronn) < 0.8 22 | # BuildRequires: rubygem(rspec) => 3.5 23 | # BuildRequires: rubygem(rspec) < 4 24 | BuildArch: noarch 25 | 26 | %description 27 | Bundler manages an application's dependencies through its entire life, across 28 | many machines, systematically and repeatably. 29 | 30 | 31 | %package doc 32 | Summary: Documentation for %{name} 33 | Group: Documentation 34 | Requires: %{name} = %{version}-%{release} 35 | BuildArch: noarch 36 | 37 | %description doc 38 | Documentation for %{name}. 39 | 40 | %prep 41 | gem unpack %{SOURCE0} 42 | 43 | %setup -q -D -T -n %{gem_name}-%{version} 44 | 45 | gem spec %{SOURCE0} -l --ruby > %{gem_name}.gemspec 46 | 47 | %build 48 | # Create the gem as gem install only works on a gem file 49 | gem build %{gem_name}.gemspec 50 | 51 | # %%gem_install compiles any C extensions and installs the gem into ./%%gem_dir 52 | # by default, so that we can move it into the buildroot in %%install 53 | %gem_install 54 | 55 | %install 56 | mkdir -p %{buildroot}%{gem_dir} 57 | cp -a .%{gem_dir}/* \ 58 | %{buildroot}%{gem_dir}/ 59 | 60 | 61 | mkdir -p %{buildroot}%{_bindir} 62 | cp -pa .%{_bindir}/* \ 63 | %{buildroot}%{_bindir}/ 64 | 65 | find %{buildroot}%{gem_instdir}/bin -type f | xargs chmod a+x 66 | find %{buildroot}%{gem_instdir}/lib/bundler/templates/newgem/bin -type f | xargs chmod 755 67 | chmod 755 %{buildroot}%{gem_instdir}/lib/bundler/templates/Executable* 68 | 69 | # Man pages are used by Bundler internally, do not remove them! 70 | mkdir -p %{buildroot}%{_mandir}/man5 71 | cp -a %{buildroot}%{gem_libdir}/bundler/man/gemfile.5 %{buildroot}%{_mandir}/man5 72 | mkdir -p %{buildroot}%{_mandir}/man1 73 | for i in bundle bundle-config bundle-exec bundle-install bundle-package bundle-platform bundle-update 74 | do 75 | cp -a %{buildroot}%{gem_libdir}/bundler/man/$i %{buildroot}%{_mandir}/man1/`echo $i.1` 76 | done 77 | 78 | # Run the test suite 79 | %check 80 | pushd .%{gem_instdir} 81 | 82 | popd 83 | 84 | %files 85 | %dir %{gem_instdir} 86 | %{_bindir}/bundle 87 | %{_bindir}/bundler 88 | %{gem_instdir}/.codeclimate.yml 89 | %exclude %{gem_instdir}/.gitignore 90 | %{gem_instdir}/.rspec 91 | %exclude %{gem_instdir}/.rubocop.yml 92 | %{gem_instdir}/.rubocop_todo.yml 93 | %exclude %{gem_instdir}/.travis.yml 94 | %{gem_instdir}/CODE_OF_CONDUCT.md 95 | %{gem_instdir}/DEVELOPMENT.md 96 | %{gem_instdir}/ISSUES.md 97 | %license %{gem_instdir}/LICENSE.md 98 | %{gem_instdir}/bin 99 | %{gem_instdir}/exe 100 | %{gem_libdir} 101 | %{gem_instdir}/man 102 | %exclude %{gem_cache} 103 | %{gem_spec} 104 | %doc %{_mandir}/man1/* 105 | %doc %{_mandir}/man5/* 106 | 107 | %files doc 108 | %doc %{gem_docdir} 109 | %doc %{gem_instdir}/CHANGELOG.md 110 | %doc %{gem_instdir}/CONTRIBUTING.md 111 | %doc %{gem_instdir}/README.md 112 | %{gem_instdir}/Rakefile 113 | %{gem_instdir}/bundler.gemspec 114 | 115 | %changelog 116 | * Mon Nov 21 2016 Tim - 1.13.6-1 117 | - Initial package 118 | -------------------------------------------------------------------------------- /dependencies/rubygem-minitest.spec: -------------------------------------------------------------------------------- 1 | # Generated from minitest-5.9.1.gem by gem2rpm -*- rpm-spec -*- 2 | %global gem_name minitest 3 | 4 | Name: rubygem-%{gem_name} 5 | Version: 5.9.1 6 | Release: 1%{?dist} 7 | Summary: minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking 8 | Group: Development/Languages 9 | License: MIT 10 | URL: https://github.com/seattlerb/minitest 11 | Source0: https://rubygems.org/gems/%{gem_name}-%{version}.gem 12 | BuildRequires: ruby(release) 13 | BuildRequires: rubygems-devel 14 | BuildRequires: ruby 15 | # BuildRequires: rubygem(hoe) => 3.15 16 | # BuildRequires: rubygem(hoe) < 4 17 | BuildArch: noarch 18 | 19 | %description 20 | minitest provides a complete suite of testing facilities supporting 21 | TDD, BDD, mocking, and benchmarking. 22 | "I had a class with Jim Weirich on testing last week and we were 23 | allowed to choose our testing frameworks. Kirk Haines and I were 24 | paired up and we cracked open the code for a few test 25 | frameworks... 26 | I MUST say that minitest is *very* readable / understandable 27 | compared to the 'other two' options we looked at. Nicely done and 28 | thank you for helping us keep our mental sanity." 29 | -- Wayne E. Seguin 30 | minitest/test is a small and incredibly fast unit testing framework. 31 | It provides a rich set of assertions to make your tests clean and 32 | readable. 33 | minitest/spec is a functionally complete spec engine. It hooks onto 34 | minitest/test and seamlessly bridges test assertions over to spec 35 | expectations. 36 | minitest/benchmark is an awesome way to assert the performance of your 37 | algorithms in a repeatable manner. Now you can assert that your newb 38 | co-worker doesn't replace your linear algorithm with an exponential 39 | one! 40 | minitest/mock by Steven Baker, is a beautifully tiny mock (and stub) 41 | object framework. 42 | minitest/pride shows pride in testing and adds coloring to your test 43 | output. I guess it is an example of how to write IO pipes too. :P 44 | minitest/test is meant to have a clean implementation for language 45 | implementors that need a minimal set of methods to bootstrap a working 46 | test suite. For example, there is no magic involved for test-case 47 | discovery. 48 | "Again, I can't praise enough the idea of a testing/specing 49 | framework that I can actually read in full in one sitting!" 50 | -- Piotr Szotkowski 51 | Comparing to rspec: 52 | rspec is a testing DSL. minitest is ruby. 53 | -- Adam Hawkins, "Bow Before MiniTest" 54 | minitest doesn't reinvent anything that ruby already provides, like: 55 | classes, modules, inheritance, methods. This means you only have to 56 | learn ruby to use minitest and all of your regular OO practices like 57 | extract-method refactorings still apply. 58 | 59 | 60 | %package doc 61 | Summary: Documentation for %{name} 62 | Group: Documentation 63 | Requires: %{name} = %{version}-%{release} 64 | BuildArch: noarch 65 | 66 | %description doc 67 | Documentation for %{name}. 68 | 69 | %prep 70 | gem unpack %{SOURCE0} 71 | 72 | %setup -q -D -T -n %{gem_name}-%{version} 73 | 74 | gem spec %{SOURCE0} -l --ruby > %{gem_name}.gemspec 75 | 76 | %build 77 | # Create the gem as gem install only works on a gem file 78 | gem build %{gem_name}.gemspec 79 | 80 | # %%gem_install compiles any C extensions and installs the gem into ./%%gem_dir 81 | # by default, so that we can move it into the buildroot in %%install 82 | %gem_install 83 | 84 | %install 85 | mkdir -p %{buildroot}%{gem_dir} 86 | cp -a .%{gem_dir}/* \ 87 | %{buildroot}%{gem_dir}/ 88 | 89 | 90 | 91 | 92 | # Run the test suite 93 | %check 94 | pushd .%{gem_instdir} 95 | 96 | popd 97 | 98 | %files 99 | %dir %{gem_instdir} 100 | %{gem_instdir}/.autotest 101 | %{gem_instdir}/Manifest.txt 102 | %{gem_instdir}/design_rationale.rb 103 | %{gem_libdir} 104 | %exclude %{gem_cache} 105 | %{gem_spec} 106 | 107 | %files doc 108 | %doc %{gem_docdir} 109 | %doc %{gem_instdir}/History.rdoc 110 | %doc %{gem_instdir}/README.rdoc 111 | %{gem_instdir}/Rakefile 112 | %{gem_instdir}/test 113 | 114 | %changelog 115 | * Mon Nov 21 2016 Tim - 5.9.1-1 116 | - Initial package 117 | -------------------------------------------------------------------------------- /dependencies/rubygem-mixlib-log.spec: -------------------------------------------------------------------------------- 1 | # Generated from mixlib-log-1.7.1.gem by gem2rpm -*- rpm-spec -*- 2 | %global gem_name mixlib-log 3 | 4 | Name: rubygem-%{gem_name} 5 | Version: 1.7.1 6 | Release: 1%{?dist} 7 | Summary: A gem that provides a simple mixin for log functionality 8 | Group: Development/Languages 9 | License: Apache-2.0 10 | URL: https://www.chef.io 11 | Source0: https://rubygems.org/gems/%{gem_name}-%{version}.gem 12 | BuildRequires: ruby(release) 13 | BuildRequires: rubygems-devel 14 | BuildRequires: ruby 15 | # BuildRequires: rubygem(rspec) => 3.4 16 | # BuildRequires: rubygem(rspec) < 4 17 | # BuildRequires: rubygem(chefstyle) => 0.3 18 | # BuildRequires: rubygem(chefstyle) < 1 19 | # BuildRequires: rubygem(cucumber) 20 | # BuildRequires: rubygem(github_changelog_generator) = 1.11.3 21 | BuildArch: noarch 22 | 23 | %description 24 | A gem that provides a simple mixin for log functionality. 25 | 26 | 27 | %package doc 28 | Summary: Documentation for %{name} 29 | Group: Documentation 30 | Requires: %{name} = %{version}-%{release} 31 | BuildArch: noarch 32 | 33 | %description doc 34 | Documentation for %{name}. 35 | 36 | %prep 37 | gem unpack %{SOURCE0} 38 | 39 | %setup -q -D -T -n %{gem_name}-%{version} 40 | 41 | gem spec %{SOURCE0} -l --ruby > %{gem_name}.gemspec 42 | 43 | %build 44 | # Create the gem as gem install only works on a gem file 45 | gem build %{gem_name}.gemspec 46 | 47 | # %%gem_install compiles any C extensions and installs the gem into ./%%gem_dir 48 | # by default, so that we can move it into the buildroot in %%install 49 | %gem_install 50 | 51 | %install 52 | mkdir -p %{buildroot}%{gem_dir} 53 | cp -a .%{gem_dir}/* \ 54 | %{buildroot}%{gem_dir}/ 55 | 56 | 57 | 58 | 59 | # Run the test suite 60 | %check 61 | pushd .%{gem_instdir} 62 | 63 | popd 64 | 65 | %files 66 | %dir %{gem_instdir} 67 | %exclude %{gem_instdir}/.gemtest 68 | %license %{gem_instdir}/LICENSE 69 | %{gem_instdir}/NOTICE 70 | %{gem_libdir} 71 | %exclude %{gem_cache} 72 | %{gem_spec} 73 | 74 | %files doc 75 | %doc %{gem_docdir} 76 | %{gem_instdir}/Gemfile 77 | %doc %{gem_instdir}/README.md 78 | %{gem_instdir}/Rakefile 79 | %{gem_instdir}/mixlib-log.gemspec 80 | %{gem_instdir}/spec 81 | 82 | %changelog 83 | * Mon Nov 21 2016 Tim - 1.7.1-1 84 | - Initial package 85 | -------------------------------------------------------------------------------- /dependencies/rubygem-sinatra.spec: -------------------------------------------------------------------------------- 1 | # Generated from sinatra-1.4.5.gem by gem2rpm -*- rpm-spec -*- 2 | %global gem_name sinatra 3 | 4 | Name: rubygem-%{gem_name} 5 | Version: 1.4.5 6 | Release: 1%{?dist} 7 | Summary: Classy web-development dressed in a DSL 8 | Group: Development/Languages 9 | License: MIT 10 | URL: http://www.sinatrarb.com/ 11 | Source0: https://rubygems.org/gems/%{gem_name}-%{version}.gem 12 | BuildRequires: ruby(release) 13 | BuildRequires: rubygems-devel 14 | BuildRequires: ruby 15 | BuildArch: noarch 16 | 17 | %description 18 | Sinatra is a DSL for quickly creating web applications in Ruby with minimal 19 | effort. 20 | 21 | 22 | %package doc 23 | Summary: Documentation for %{name} 24 | Group: Documentation 25 | Requires: %{name} = %{version}-%{release} 26 | BuildArch: noarch 27 | 28 | %description doc 29 | Documentation for %{name}. 30 | 31 | %prep 32 | gem unpack %{SOURCE0} 33 | 34 | %setup -q -D -T -n %{gem_name}-%{version} 35 | 36 | gem spec %{SOURCE0} -l --ruby > %{gem_name}.gemspec 37 | 38 | %build 39 | # Create the gem as gem install only works on a gem file 40 | gem build %{gem_name}.gemspec 41 | 42 | # %%gem_install compiles any C extensions and installs the gem into ./%%gem_dir 43 | # by default, so that we can move it into the buildroot in %%install 44 | %gem_install 45 | 46 | %install 47 | mkdir -p %{buildroot}%{gem_dir} 48 | cp -a .%{gem_dir}/* \ 49 | %{buildroot}%{gem_dir}/ 50 | 51 | 52 | 53 | 54 | # Run the test suite 55 | %check 56 | pushd .%{gem_instdir} 57 | 58 | popd 59 | 60 | %files 61 | %dir %{gem_instdir} 62 | %exclude %{gem_instdir}/.yardopts 63 | %{gem_instdir}/CHANGES 64 | %license %{gem_instdir}/LICENSE 65 | %{gem_libdir} 66 | %exclude %{gem_cache} 67 | %{gem_spec} 68 | 69 | %files doc 70 | %doc %{gem_docdir} 71 | %doc %{gem_instdir}/AUTHORS 72 | %{gem_instdir}/Gemfile 73 | %doc %{gem_instdir}/README.de.md 74 | %doc %{gem_instdir}/README.es.md 75 | %doc %{gem_instdir}/README.fr.md 76 | %doc %{gem_instdir}/README.hu.md 77 | %doc %{gem_instdir}/README.ja.md 78 | %doc %{gem_instdir}/README.ko.md 79 | %doc %{gem_instdir}/README.md 80 | %doc %{gem_instdir}/README.pt-br.md 81 | %doc %{gem_instdir}/README.pt-pt.md 82 | %doc %{gem_instdir}/README.ru.md 83 | %doc %{gem_instdir}/README.zh.md 84 | %{gem_instdir}/Rakefile 85 | %{gem_instdir}/examples 86 | %{gem_instdir}/sinatra.gemspec 87 | %{gem_instdir}/test 88 | 89 | %changelog 90 | * Mon Nov 21 2016 Tim - 1.4.5-1 91 | - Initial package 92 | -------------------------------------------------------------------------------- /dependencies/rubygem-tilt.spec: -------------------------------------------------------------------------------- 1 | %global gem_name tilt 2 | 3 | %global bootstrap 1 4 | 5 | Summary: Generic interface to multiple Ruby template engines 6 | Name: rubygem-%{gem_name} 7 | Version: 1.4.1 8 | Release: 3%{?dist} 9 | Group: Development/Languages 10 | License: MIT 11 | URL: http://github.com/rtomayko/%{gem_name} 12 | Source0: http://rubygems.org/gems/%{gem_name}-%{version}.gem 13 | BuildRequires: ruby(release) 14 | BuildRequires: rubygems-devel 15 | BuildRequires: ruby 16 | %if 0%{bootstrap} < 1 17 | BuildRequires: rubygem(creole) 18 | BuildRequires: rubygem(minitest) 19 | BuildRequires: rubygem(nokogiri) 20 | BuildRequires: rubygem(erubis) 21 | BuildRequires: rubygem(haml) 22 | BuildRequires: rubygem(builder) 23 | BuildRequires: rubygem(maruku) 24 | BuildRequires: rubygem(RedCloth) 25 | BuildRequires: rubygem(redcarpet) 26 | BuildRequires: rubygem(coffee-script) 27 | BuildRequires: rubygem(therubyracer) 28 | BuildRequires: rubygem(wikicloth) 29 | 30 | # BuildRequires: rubygem(asciidoctor) 31 | #BuildRequires: rubygem(kramdown) 32 | # Markaby test fails. It is probably due to rather old version found in Fedora. 33 | # https://github.com/rtomayko/tilt/issues/96 34 | # BuildRequires: rubygem(markaby) 35 | #BuildRequires: rubygem(rdiscount) 36 | %endif 37 | %if 0%{?fc20} || 0%{?el7} 38 | Provides: rubygem(%{gem_name}) = %{version} 39 | %endif 40 | 41 | BuildArch: noarch 42 | 43 | %description 44 | Generic interface to multiple Ruby template engines 45 | 46 | 47 | %package doc 48 | Summary: Documentation for %{name} 49 | Group: Documentation 50 | Requires:%{name} = %{version}-%{release} 51 | 52 | %description doc 53 | Documentation for %{name} 54 | 55 | %prep 56 | %setup -q -c -T 57 | %gem_install -n %{SOURCE0} 58 | 59 | pushd .%{gem_instdir} 60 | popd 61 | 62 | %build 63 | 64 | %install 65 | mkdir -p %{buildroot}%{gem_dir} 66 | cp -a .%{gem_dir}/* \ 67 | %{buildroot}%{gem_dir}/ 68 | 69 | mkdir -p %{buildroot}%{_bindir} 70 | cp -a .%{_bindir}/* \ 71 | %{buildroot}%{_bindir}/ 72 | 73 | find %{buildroot}%{gem_instdir}/bin -type f | xargs chmod a+x 74 | 75 | %check 76 | %if 0%{bootstrap} < 1 77 | pushd %{buildroot}%{gem_instdir} 78 | popd 79 | %endif 80 | 81 | %files 82 | %dir %{gem_instdir} 83 | %{_bindir}/%{gem_name} 84 | %exclude %{gem_instdir}/%{gem_name}.gemspec 85 | %exclude %{gem_instdir}/.* 86 | %exclude %{gem_instdir}/Gemfile 87 | %{gem_instdir}/bin 88 | %{gem_libdir} 89 | %doc %{gem_instdir}/COPYING 90 | %doc %{gem_instdir}/README.md 91 | %doc %{gem_instdir}/TEMPLATES.md 92 | %exclude %{gem_cache} 93 | %{gem_spec} 94 | 95 | %files doc 96 | %doc %{gem_docdir} 97 | %doc %{gem_instdir}/CHANGELOG.md 98 | %doc %{gem_instdir}/HACKING 99 | %{gem_instdir}/Rakefile 100 | %{gem_instdir}/test 101 | 102 | 103 | %changelog 104 | * Mon Nov 21 2016 Tim - 1.4.1-1 105 | - Initial package 106 | -------------------------------------------------------------------------------- /docs/alerts.adoc: -------------------------------------------------------------------------------- 1 | // vim: tw=79 2 | = Alerts 3 | :toc: 4 | 5 | == List alerts 6 | 7 | Retrieve list of alerts 8 | 9 | Sample Request 10 | 11 | ---------- 12 | curl -XGET -H "Authorization: Bearer 13 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" -H "Content-Type: application/json" http://127.0.0.1/api/1.0/alerts 14 | ---------- 15 | 16 | Sample Response 17 | 18 | ---------- 19 | [{ 20 | "source": "node_agent", 21 | "time_stamp": "2017-09-11T11:24:41.273828+00:00", 22 | "ackedby": "", 23 | "classification": "cluster", 24 | "hash": "2eca2bfd76f4db0412618bf8ed95da07", 25 | "resource": "job_status", 26 | "updated_at": "2017-09-11 11:24:42.811869+00:00", 27 | "ack_comment": "", 28 | "acked": "False", 29 | "pid": "20850", 30 | "severity": "INFO", 31 | "delivered": "False", 32 | "node_id": "35b27276-9058-4c09-b64b-9d239ed262cb", 33 | "significance": "", 34 | "tags": { 35 | "cluster_name": "gluster-4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", 36 | "integration_id": "d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", 37 | "message": "Job finished successfully (job_id: d8f72e0b-bd94-4166-9614-2b95a8db5841)", 38 | "fqdn": "dhcp41-173.lab.eng.blr.redhat.com", 39 | "sds_name": "gluster" 40 | }, 41 | "acked_at": "", 42 | "alert_id": "fd47f3fb-ca49-4bb4-a590-3e70330cd9d3", 43 | "alert_type": "STATUS", 44 | "current_value": "finished" 45 | }, { 46 | "current_value": "failed", 47 | "tags": { 48 | "cluster_name": "gluster-4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", 49 | "integration_id": "d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", 50 | "message": "Job failed (job_id: 0b81e204-01eb-4db3-b73d-93f33b55a0fa)", 51 | "fqdn": "dhcp42-54.lab.eng.blr.redhat.com", 52 | "sds_name": null 53 | }, 54 | "delivered": "False", 55 | "hash": "8aab0b98d1469f2886b0cacdd98b37ff", 56 | "source": "monitoring_integration", 57 | "updated_at": "2017-09-11 11:55:33.806011+00:00", 58 | "ack_comment": "", 59 | "ackedby": "", 60 | "classification": "cluster", 61 | "node_id": "e8334ec7-9b02-42d0-b298-4f2b836fbcd6", 62 | "resource": "job_status", 63 | "severity": "WARNING", 64 | "significance": "", 65 | "time_stamp": "2017-09-11T11:55:30.623566+00:00", 66 | "acked": "False", 67 | "alert_id": "b970e945-0c77-4111-b8cc-3c41cab7cc24", 68 | "alert_type": "STATUS", 69 | "acked_at": "", 70 | "pid": "32266" 71 | }] 72 | ---------- 73 | 74 | == Single alert 75 | 76 | Retrieve single alert 77 | 78 | Sample Request 79 | 80 | ---------- 81 | curl -XGET -H "Authorization: Bearer 82 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" -H "Content-Type: application/json" \ 83 | http://127.0.0.1/api/1.0/alerts/fd47f3fb-ca49-4bb4-a590-3e70330cd9d3 84 | ---------- 85 | 86 | Sample Response 87 | 88 | ---------- 89 | { 90 | "source": "node_agent", 91 | "time_stamp": "2017-09-11T11:24:41.273828+00:00", 92 | "ackedby": "", 93 | "classification": "cluster", 94 | "hash": "2eca2bfd76f4db0412618bf8ed95da07", 95 | "resource": "job_status", 96 | "updated_at": "2017-09-11 11:24:42.811869+00:00", 97 | "ack_comment": "", 98 | "acked": "False", 99 | "pid": "20850", 100 | "severity": "INFO", 101 | "delivered": "False", 102 | "node_id": "35b27276-9058-4c09-b64b-9d239ed262cb", 103 | "significance": "", 104 | "tags": { 105 | "cluster_name": "gluster-4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", 106 | "integration_id": "d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", 107 | "message": "Job finished successfully (job_id: d8f72e0b-bd94-4166-9614-2b95a8db5841)", 108 | "fqdn": "dhcp41-173.lab.eng.blr.redhat.com", 109 | "sds_name": "gluster" 110 | }, 111 | "acked_at": "", 112 | "alert_id": "fd47f3fb-ca49-4bb4-a590-3e70330cd9d3", 113 | "alert_type": "STATUS", 114 | "current_value": "finished" 115 | } 116 | ---------- 117 | 118 | -------------------------------------------------------------------------------- /docs/authentication.adoc: -------------------------------------------------------------------------------- 1 | // vim: tw=79 2 | = Authentication 3 | :toc: 4 | 5 | == Login 6 | 7 | Login with a existing username and password. 8 | 9 | Sample Request 10 | 11 | ---------- 12 | curl -H 'Content-Type: application/json' -d '{"username":"admin", 13 | "password": "pass1234"}' http://127.0.0.1/api/1.0/login 14 | ---------- 15 | 16 | Sample Response 17 | 18 | ---------- 19 | Status: 201 Created 20 | 21 | { 22 | "access_token":"c17891f009a00994a9dcfe7f977b2f244b43a4a920e5" 23 | } 24 | ---------- 25 | 26 | The access token can be used as a `Bearer` token in subsequent requests to recognize the current user. 27 | 28 | == Logout 29 | 30 | Logout with a existing authentication token 31 | 32 | Sample Request 33 | 34 | ---------- 35 | curl -H 'Content-Type: application/json' -H 'Authorization: Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8' -XDELETE http://127.0.0.1/api/1.0/logout 36 | ---------- 37 | 38 | Sample Response 39 | 40 | ---------- 41 | Status: 200 OK 42 | {} 43 | ---------- 44 | -------------------------------------------------------------------------------- /docs/authorizaton.adoc: -------------------------------------------------------------------------------- 1 | // vim: tw=79 2 | = Authorization 3 | 4 | Tendrl provides authorization based on Roles 5 | 6 | == Roles 7 | 8 | .Role Actions 9 | 10 | |=== 11 | |Role | Actions | Extra 12 | 13 | |`admin` 14 | |`GET, POST, PUT, DELETE` 15 | |`Manage Users` 16 | 17 | |`normal` 18 | |`GET, POST, PUT, DELETE` 19 | | - 20 | 21 | |`limited` 22 | |`GET` 23 | | `-` 24 | |=== 25 | -------------------------------------------------------------------------------- /docs/jobs.adoc: -------------------------------------------------------------------------------- 1 | // vim: tw=79 2 | = Jobs 3 | :toc: 4 | 5 | == List jobs 6 | 7 | List all jobs created. 8 | 9 | Sample Request 10 | 11 | ---------- 12 | curl -XGET -H "Authorization: Bearer 13 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" 14 | http://127.0.0.1/api/1.0/jobs 15 | ---------- 16 | 17 | Sample Response 18 | 19 | ---------- 20 | Status: 200 OK 21 | [{ 22 | "job_id": "165bd201-2bee-44e9-a706-321290db798c", 23 | "status": "finished", 24 | "flow": "ImportCluster", 25 | "parameters": { 26 | "integration_id": "717315a1-2a49-4cb7-826b-d2aa3bc4721a", 27 | "DetectedCluster.sds_pkg_name": "ceph", 28 | "node_ids": ["3b6eb27f-3e83-4751-9d45-85a989ae2b25"], 29 | "sds_version": "10.2.5", 30 | "sds_name": "ceph 10.2.5", 31 | "sds_type": "ceph", 32 | "hosts": [{ 33 | "release": "ceph 10.2.5", 34 | "role": "Monitor", 35 | "name": "dhcp43-203.lab.eng.blr.redhat.com" 36 | }], 37 | "cluster_id": "c221ccdb-51d6-4b57-9f10-bcf30c7fa351", 38 | "TendrlContext.integration_id": "717315a1-2a49-4cb7-826b-d2aa3bc4721a", 39 | "Node[]": ["3b6eb27f-3e83-4751-9d45-85a989ae2b25"] 40 | }, 41 | "created_at": "2017-01-26T08:04:25Z", 42 | "status_url": "/jobs/165bd201-2bee-44e9-a706-321290db798c/status", 43 | "messages_url": "/jobs/165bd201-2bee-44e9-a706-321290db798c/messages" 44 | "output_url": "/jobs/165bd201-2bee-44e9-a706-321290db798c/output" 45 | }] 46 | ---------- 47 | 48 | == Job Details 49 | 50 | Individual job detail. 51 | 52 | Sample Request 53 | 54 | ---------- 55 | curl -XGET -H "Authorization: Bearer 56 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" 57 | http://127.0.0.1/api/1.0/jobs/1cb0694c-8041-4576-95ee-f01f07738ff9 58 | ---------- 59 | 60 | Sample Response 61 | 62 | ---------- 63 | Status: 200 OK 64 | { 65 | "job_id": "165bd201-2bee-44e9-a706-321290db798c", 66 | "status": "finished", 67 | "flow": "ImportCluster", 68 | "parameters": { 69 | "integration_id": "717315a1-2a49-4cb7-826b-d2aa3bc4721a", 70 | "DetectedCluster.sds_pkg_name": "ceph", 71 | "node_ids": ["3b6eb27f-3e83-4751-9d45-85a989ae2b25"], 72 | "sds_version": "10.2.5", 73 | "sds_name": "ceph 10.2.5", 74 | "sds_type": "ceph", 75 | "hosts": [{ 76 | "release": "ceph 10.2.5", 77 | "role": "Monitor", 78 | "name": "dhcp43-203.lab.eng.blr.redhat.com" 79 | }], 80 | "request_id": "nodes/3b6eb27f-3e83-4751-9d45-85a989ae2b25/_jobs/tendrl.node_agent.flows.import_cluster.ImportCluster_9c9e5d67-d7e8-472d-9f88-ed613a200f7b", 81 | "cluster_id": "c221ccdb-51d6-4b57-9f10-bcf30c7fa351", 82 | "TendrlContext.integration_id": "717315a1-2a49-4cb7-826b-d2aa3bc4721a", 83 | "Node[]": ["3b6eb27f-3e83-4751-9d45-85a989ae2b25"] 84 | }, 85 | "created_at": "2017-01-26T08:04:25Z", 86 | "status_url": "/jobs/165bd201-2bee-44e9-a706-321290db798c/status", 87 | "messages_url": "/jobs/165bd201-2bee-44e9-a706-321290db798c/messages" 88 | 89 | } 90 | ---------- 91 | 92 | 93 | -------------------------------------------------------------------------------- /docs/nodes.adoc: -------------------------------------------------------------------------------- 1 | // vim: tw=79 2 | = Nodes 3 | :toc: 4 | 5 | == List Nodes 6 | 7 | Sample Request 8 | 9 | ---------- 10 | curl -XGET -H "Authorization: Bearer 11 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" 12 | http://127.0.0.1/api/1.0/nodes 13 | ---------- 14 | 15 | Sample Response 16 | 17 | ---------- 18 | { 19 | "nodes": [{ 20 | "hash": "ac36daa7f4c5d46aee1aeb88b7867505", 21 | "tags": ["tendrl/integration/monitoring", "tendrl/central-store", "tendrl/node_e8334ec7-9b02-42d0-b298-4f2b836fbcd6", "tendrl/server", "tendrl/monitor", "tendrl/node"], 22 | "sync_status": "done", 23 | "fqdn": "dhcp-1.lab.tendrl", 24 | "updated_at": "2017-09-12 08:07:32.846995+00:00", 25 | "node_id": "e8334ec7-9b02-42d0-b298-4f2b836fbcd6", 26 | "last_sync": "2017-09-12 08:07:32.796142+00:00", 27 | "status": "UP", 28 | "cluster": { 29 | "cluster_name": "", 30 | "cluster_id": "", 31 | "integration_id": "", 32 | "hash": "f8952d244ec9d84e80f4bb993e409d85", 33 | "sds_version": "", 34 | "updated_at": "2017-09-11 11:20:24.776424+00:00", 35 | "sds_name": "" 36 | } 37 | }, { 38 | "hash": "26145a8f76e6d93cf7f4732397a6f7e9", 39 | "tags": ["provisioner/d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", "tendrl/integration/gluster", "tendrl/integration/d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", "tendrl/node_35b27276-9058-4c09-b64b-9d239ed262cb", "gluster/server", "detected_cluster/4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", "tendrl/node"], 40 | "sync_status": "done", 41 | "fqdn": "dhcp-2.lab.tendrl", 42 | "updated_at": "2017-09-12 08:07:24.759475+00:00", 43 | "node_id": "35b27276-9058-4c09-b64b-9d239ed262cb", 44 | "last_sync": "2017-09-12 08:07:24.523445+00:00", 45 | "status": "UP", 46 | "cluster": { 47 | "cluster_id": "4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", 48 | "integration_id": "d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", 49 | "hash": "a816b24b564c64e91243764447b93521", 50 | "sds_version": "4.0dev", 51 | "updated_at": "2017-09-12 05:46:29.138406+00:00", 52 | "sds_name": "gluster", 53 | "cluster_name": "gluster-4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8" 54 | } 55 | }, { 56 | "status": "UP", 57 | "hash": "5193c3ca1277fd5d31ff83c2d2d8b10b", 58 | "tags": ["tendrl/integration/gluster", "tendrl/integration/d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", "tendrl/node_66898e03-4db2-4898-b21e-6f034ba10077", "gluster/server", "detected_cluster/4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", "tendrl/node"], 59 | "sync_status": "in_progress", 60 | "fqdn": "dhcp-3.lab.tendrl", 61 | "updated_at": "2017-09-12 08:07:26.887490+00:00", 62 | "node_id": "66898e03-4db2-4898-b21e-6f034ba10077", 63 | "last_sync": "2017-09-12 08:07:16.364497+00:00", 64 | "cluster": { 65 | "integration_id": "d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", 66 | "hash": "a816b24b564c64e91243764447b93521", 67 | "sds_version": "4.0dev", 68 | "updated_at": "2017-09-12 05:46:36.804847+00:00", 69 | "sds_name": "gluster", 70 | "cluster_name": "gluster-4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", 71 | "cluster_id": "4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8" 72 | } 73 | }, { 74 | "sync_status": "done", 75 | "fqdn": "dhcp-4.lab.tendrl", 76 | "updated_at": "2017-09-12 08:07:32.896069+00:00", 77 | "node_id": "2f8dcb72-43d8-4ee6-8a12-dfa93a8edbf3", 78 | "last_sync": "2017-09-12 08:07:32.861939+00:00", 79 | "status": "UP", 80 | "hash": "54a60ef75240462009e4f99514953fd9", 81 | "tags": ["tendrl/integration/gluster", "tendrl/integration/d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", "tendrl/node_2f8dcb72-43d8-4ee6-8a12-dfa93a8edbf3", "gluster/server", "detected_cluster/4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", "tendrl/node"], 82 | "cluster": { 83 | "cluster_id": "4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", 84 | "integration_id": "d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", 85 | "hash": "a816b24b564c64e91243764447b93521", 86 | "sds_version": "4.0dev", 87 | "updated_at": "2017-09-12 05:46:42.791807+00:00", 88 | "sds_name": "gluster", 89 | "cluster_name": "gluster-4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8" 90 | } 91 | }] 92 | } 93 | ---------- 94 | 95 | == List Nodes per Cluster 96 | 97 | Sample Request 98 | 99 | ---------- 100 | curl -XGET -H "Authorization: Bearer 101 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" 102 | http://127.0.0.1/api/1.0/clusters/d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5/nodes 103 | ---------- 104 | 105 | Sample Response 106 | 107 | ---------- 108 | { 109 | "nodes": [{ 110 | "last_sync": "2017-09-12 08:12:00.237635+00:00", 111 | "status": "UP", 112 | "hash": "55af3ebe7246f61590d788856dfb40f4", 113 | "tags": ["provisioner/d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", "tendrl/integration/gluster", "tendrl/integration/d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", "tendrl/node_35b27276-9058-4c09-b64b-9d239ed262cb", "gluster/server", "detected_cluster/4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", "tendrl/node"], 114 | "sync_status": "done", 115 | "fqdn": "dhcp-0.lab.tendrl", 116 | "updated_at": "2017-09-12 08:12:00.507571+00:00", 117 | "node_id": "35b27276-9058-4c09-b64b-9d239ed262cb", 118 | "cluster": {} 119 | }, { 120 | "fqdn": "dhcp-2.lab.tendrl", 121 | "updated_at": "2017-09-12 08:11:58.143902+00:00", 122 | "node_id": "66898e03-4db2-4898-b21e-6f034ba10077", 123 | "last_sync": "2017-09-12 08:11:57.845365+00:00", 124 | "status": "UP", 125 | "hash": "fed897d2c36cdca58fd4daea0e6b1fff", 126 | "tags": ["tendrl/integration/gluster", "tendrl/integration/d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", "tendrl/node_66898e03-4db2-4898-b21e-6f034ba10077", "gluster/server", "detected_cluster/4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", "tendrl/node"], 127 | "sync_status": "done", 128 | "cluster": {} 129 | }, { 130 | "node_id": "2f8dcb72-43d8-4ee6-8a12-dfa93a8edbf3", 131 | "last_sync": "2017-09-12 08:11:52.715507+00:00", 132 | "status": "UP", 133 | "hash": "1940162a29e1ea6edca6b84b095fc1f5", 134 | "tags": ["tendrl/integration/gluster", "tendrl/integration/d9c4b7de-25d9-43f4-8bdd-0ebf4effe9e5", "tendrl/node_2f8dcb72-43d8-4ee6-8a12-dfa93a8edbf3", "gluster/server", "detected_cluster/4994728774f7dc9b895a77342e55afd2617587d1c8233af365101923fd02c6f8", "tendrl/node"], 135 | "sync_status": "done", 136 | "fqdn": "dhcp-1.lab.tendrl", 137 | "updated_at": "2017-09-12 08:11:53.254564+00:00", 138 | "cluster": {} 139 | }] 140 | } 141 | ---------- 142 | 143 | == List Bricks per Nodes 144 | 145 | Sample Request 146 | 147 | ---------- 148 | curl -XGET -H "Authorization: Bearer 149 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" 150 | http://127.0.0.1/api/1.0/clusters/5291c055-70d3-4450-9769-2f6fd4932afb/nodes/35b27276-9058-4c09-b64b-9d239ed262cb/bricks 151 | ---------- 152 | 153 | Sample Response 154 | 155 | ---------- 156 | Status: 200 OK 157 | { 158 | "bricks": [{ 159 | "vg": "cl_dhcp41-173", 160 | "disk_count": "", 161 | "hash": "4c1da7597edf8a4edc6b669d8d9c4d41", 162 | "name": "dhcp-3.lab.tendrl:_root_gluster_bricks_vol1_b1", 163 | "utilization": { 164 | "metadata_used": null, 165 | "used_percent": 10.175191615829817, 166 | "thinpool_used_percent": null, 167 | "used": 1364197376, 168 | "free_inode": 6513087, 169 | "used_inode": 38465, 170 | "used_percent_inode": 0.5871127940371963, 171 | "free": 12042895360, 172 | "total_inode": 6551552, 173 | "mount_point": "/", 174 | "metadata_used_percent": null, 175 | "metadata_free": null, 176 | "thinpool_used": null, 177 | "total": 13407092736, 178 | "thinpool_size": null, 179 | "thinpool_free": null, 180 | "metadata_size": null 181 | }, 182 | "is_arbiter": "", 183 | "node_id": "35b27276-9058-4c09-b64b-9d239ed262cb", 184 | "size": "13417578496", 185 | "hostname": "dhcp-3.lab.tendrl", 186 | "lv": "cl_dhcp41-173-root", 187 | "pv": [], 188 | "used": "True", 189 | "vol_id": "2f99fd35-957b-4fe5-b13c-0255722a8c80", 190 | "brick_path": "dhcp-3.lab.tendrl:/root/gluster_bricks/vol1_b1", 191 | "client_count": "", 192 | "mount_path": "/", 193 | "updated_at": "2017-09-12 08:05:35.945477+00:00", 194 | "brick_dir": "root_gluster_bricks_vol1_b1", 195 | "sequence_number": "1", 196 | "port": "49152", 197 | "stripe_size": "", 198 | "vol_name": "vol1", 199 | "devices": [], 200 | "fqdn": "dhcp-3.lab.tendrl", 201 | "mount_opts": "", 202 | "pool": "", 203 | "disk_type": "", 204 | "filesystem_type": "", 205 | "status": "Started", 206 | "brick_id": "root_gluster_bricks_vol1_b1" 207 | }, { 208 | "brick_dir": "root_gluster_bricks_vol3_b1", 209 | "sequence_number": "1", 210 | "vg": "cl_dhcp41-173", 211 | "mount_opts": "", 212 | "status": "Stopped", 213 | "vol_name": "vol3", 214 | "hash": "1d4c2a52b57ca49928ecbcf150f704a9", 215 | "hostname": "dhcp-2.lab.tendrl", 216 | "is_arbiter": "", 217 | "used": "True", 218 | "devices": [], 219 | "disk_count": "", 220 | "node_id": "35b27276-9058-4c09-b64b-9d239ed262cb", 221 | "pool": "", 222 | "pv": [], 223 | "updated_at": "2017-09-12 08:06:16.114556+00:00", 224 | "filesystem_type": "", 225 | "lv": "cl_dhcp41-173-root", 226 | "port": "0", 227 | "brick_path": "dhcp-2.lab.tendrl:/root/gluster_bricks/vol3_b1", 228 | "client_count": "", 229 | "disk_type": "", 230 | "size": "13417578496", 231 | "stripe_size": "", 232 | "vol_id": "3ea3d010-c6ca-41f5-ab5e-9c244e244a4e", 233 | "fqdn": "dhcp-2.lab.tendrl", 234 | "mount_path": "/", 235 | "name": "dhcp-2.lab.tendrl:_root_gluster_bricks_vol3_b1", 236 | "utilization": { 237 | "metadata_used": null, 238 | "used_percent": 10.175191615829817, 239 | "thinpool_used_percent": null, 240 | "used": 1364197376, 241 | "free_inode": 6513087, 242 | "used_inode": 38465, 243 | "used_percent_inode": 0.5871127940371963, 244 | "free": 12042895360, 245 | "total_inode": 6551552, 246 | "mount_point": "/", 247 | "metadata_used_percent": null, 248 | "metadata_free": null, 249 | "thinpool_used": null, 250 | "total": 13407092736, 251 | "thinpool_size": null, 252 | "thinpool_free": null, 253 | "metadata_size": null 254 | }, 255 | "brick_id": "root_gluster_bricks_vol3_b1" 256 | }] 257 | } 258 | ---------- 259 | 260 | -------------------------------------------------------------------------------- /docs/notifications.adoc: -------------------------------------------------------------------------------- 1 | // vim: tw=79 2 | = Notifications 3 | :toc: 4 | 5 | == List notifications 6 | 7 | Retrieve list of notifications 8 | 9 | Sample Request 10 | 11 | ---------- 12 | curl -XGET -H "Authorization: Bearer 13 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" -H 14 | "Content-Type: application/json" http://127.0.0.1/api/v1/notifications 15 | ---------- 16 | 17 | Sample Response 18 | 19 | ---------- 20 | [{ 21 | "priority": "notice", 22 | "timestamp": "2017-09-12T08:06:48.281640+00:00", 23 | "message_id": "3e533a8a-ad7a-4e08-b7af-cf3662df77a5", 24 | "message": "Job finished successfully (job_id: a5a29169-36ee-4097-940d-b4cbb1fbfb45)" 25 | }, { 26 | "message_id": "51da6900-67c6-406f-b547-042f21cf3d3f", 27 | "message": "Job failed (job_id: 03069e37-3326-40cc-9e78-f168175228a7)", 28 | "timestamp": "2017-09-12T12:52:27.653265+00:00", 29 | "priority": "notice" 30 | }, { 31 | "message_id": "4cd9a212-da90-4bbd-9218-6f645fffe632", 32 | "priority": "notice", 33 | "message": "Status of brick: dhcp-1.lab.tendrl:/root/bricks/v3 under volume test-v3 changed from Stopped to Started", 34 | "timestamp": "2017-09-12T12:53:19.973138+00:00" 35 | }, { 36 | "timestamp": "2017-09-12T12:54:01.571057+00:00", 37 | "message": "Job failed (job_id: 9dffc4a7-59b8-4936-8aff-99c7423325dc)", 38 | "message_id": "f7768473-6f71-475e-bd27-7fbe8f5f59eb", 39 | "priority": "notice" 40 | }] 41 | ---------- 42 | -------------------------------------------------------------------------------- /docs/overview.adoc: -------------------------------------------------------------------------------- 1 | // vim: tw=79 2 | = Overview 3 | ---- 4 | :toc: 5 | ---- 6 | 7 | Tendrl provides APIs accessible via HTTP(s). 8 | The API exposes operations for managing multiple enterprise storage providers 9 | with a single unified interface. 10 | 11 | 12 | Most API endpoints provides a list of actions and/or operations which can be 13 | performed depending on the nature of the resource. This approach prevents the 14 | consumer from manually constructing the URL's and also automatically adapting 15 | to functanality which might be added later or removed. 16 | 17 | The API provides actions and attributes for each kind of resource which 18 | contains mandatory attributes, optional attributes and validation rules which 19 | need to be satisfied to create a valid resource. 20 | 21 | === Versioning 22 | The API will support versioning when there are breaking changes across 23 | releases. 24 | 25 | The current version is 1.0 26 | 27 | The version numbers are a part of the url so the base url for the current 28 | version would be 29 | -------------- 30 | curl http://127.0.0.1/api/1.0 31 | -------------- 32 | 33 | === JSON Schema 34 | 35 | The API can be accessed over http(s) and all the data is sent and received over 36 | json. 37 | 38 | Every request needs to set the content-type header to application/json. 39 | ---------- 40 | curl -H "Content-Type: application/json" http://127.0.0.1/api/1.0/ping 41 | ---------- 42 | 43 | ==== Timestamps 44 | 45 | All timestamps will be in iso8601 format. 46 | ---------- 47 | YYYY-MM-DDTHH:MM:SSZ 48 | ---------- 49 | === Client Errors 50 | 51 | There are three types of client errors 52 | 53 | 1) Sending a invalid JSON will result in a `400 Bad Request` response. 54 | 55 | 2) Sending wrong type of JSON values will result in a `400 Bad Request` 56 | response. 57 | 58 | 3) Sending invalid fields will result in a `422 Unprocessable Entity` 59 | response. 60 | 61 | === HTTP Verbs 62 | 63 | The API expects the appropriate HTTP verbs for every action. 64 | 65 | `HEAD` to retrieve the HTTP header info. 66 | 67 | `GET` to retrieve a resource or multiple resources. 68 | 69 | `POST` to create a new resource. 70 | 71 | `PUT` to update a single resource or collections. 72 | 73 | `DELETE` to delete a resource 74 | 75 | === Authentication 76 | 77 | The API token needs to be sent in every request in the header 78 | `Authorization: Tendrl `. 79 | 80 | ---------- 81 | curl -H "Authorization: Tendrl " http://127.0.0.1/api/1.0 82 | ---------- 83 | 84 | === User Agent 85 | 86 | Every API request must send a `User-Agent` property in the header to identity 87 | the users. The User-Agent property should be the name of your application. 88 | 89 | ---------- 90 | curl -A "User-Agent: " 91 | ---------- 92 | 93 | === Timezones 94 | 95 | Every API request will use the user defined time zone as default. 96 | If you need the response in a specific time zone, you will have to send a 97 | `Time-Zone` header in the request which will be used to respond. 98 | 99 | ---------- 100 | curl -H "Time-Zone: Asia/Kolkata" -XGET http://127.0.0.1/api/1.0/ 101 | ---------- 102 | 103 | === Rate limiting 104 | 105 | There is a limit of 5,000 requests per hour. 106 | Exceeding the rate limit responds back with a `403 Forbidden` response. 107 | 108 | === Queuing 109 | 110 | Some operations might take longer to complete, in which case the server 111 | responds back with a job key and a `202 Accepted` response. 112 | The job key can be used to track the status of the requested operation. 113 | -------------------------------------------------------------------------------- /docs/users.adoc: -------------------------------------------------------------------------------- 1 | // vim: tw=79 2 | = Users 3 | :toc: 4 | 5 | == Create Admin User 6 | 7 | Generate a default Tendrl admin user. 8 | 9 | Run the following command from the root of the project. 10 | 11 | ---------- 12 | cd /usr/share/tendrl-api 13 | RACK_ENV=production rake etcd:load_admin 14 | ---------- 15 | 16 | The command will generate a admin user with a password. 17 | 18 | == Create User 19 | 20 | Create a new user. 21 | 22 | Sample Request 23 | 24 | ---------- 25 | curl -H 'Content-Type: application/json' -H 'Authorization: Bearer 26 | d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8' 27 | -XPOST -d '{"name":"Tom Hardy", "username":"thardy", 28 | "email":"thardy@tendrl.org", "role":"admin", "password":"pass1234", 29 | "email_notifications": true}' 30 | http://127.0.0.1/api/1.0/users 31 | ---------- 32 | 33 | Sample Response 34 | 35 | ---------- 36 | Status: 201 Created 37 | 38 | { 39 | "email": "thardy@tendrl.org", 40 | "username": "thardy", 41 | "name": "Tom Hardy", 42 | "role": "admin", 43 | "email_notifications": true 44 | } 45 | ---------- 46 | 47 | == Update User 48 | 49 | Update a existing user. 50 | 51 | Sample Request 52 | 53 | ---------- 54 | curl -H 'Content-Type: application/json' -H 'Authorization: Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8' -XPUT -d '{"name":"Tom Hardee", "username":"thardy", "email":"thardy@tendrl.org", "role":"normal"}' http://127.0.0.1/api/1.0/users/thardy 55 | ---------- 56 | 57 | Sample Response 58 | 59 | ---------- 60 | Status: 200 OK 61 | { 62 | "email": "thardy@tendrl.org", 63 | "username": "thardy", 64 | "name": "Tom Hardee", 65 | "role": "normal", 66 | "email_notifications": true 67 | } 68 | ---------- 69 | 70 | == Delete User 71 | 72 | Delete a existing user. 73 | 74 | Sample Request 75 | 76 | ---------- 77 | curl -H 'Content-Type: application/json' -H 'Authorization: Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8' -XDELETE http://127.0.0.1/api/1.0/users/thardy 78 | ---------- 79 | 80 | Sample Response 81 | 82 | ---------- 83 | Status: 200 OK 84 | {} 85 | ---------- 86 | 87 | == List Users 88 | 89 | List existing users 90 | 91 | Sample Request 92 | 93 | ---------- 94 | curl -H 'Content-Type: application/json' -H 'Authorization: Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8' -XGET http://127.0.0.1/api/1.0/users 95 | ---------- 96 | 97 | Sample Response 98 | 99 | ---------- 100 | Status: 200 OK 101 | [{ 102 | "email": "admin@tendrl.org", 103 | "username": "admin", 104 | "name": "Admin", 105 | "role": "admin", 106 | "email_notitifications": true 107 | }, { 108 | "email": "thardy@tendrl.org", 109 | "username": "thardy", 110 | "name": "Tom Hardy", 111 | "role": "admin", 112 | "email_notifications": false 113 | }] 114 | ---------- 115 | 116 | == Single User 117 | 118 | Retrieve single user 119 | 120 | Sample Request 121 | 122 | ---------- 123 | curl -H 'Authorization: Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8' -XGET http://127.0.0.1/api/1.0/users/thardy 124 | ---------- 125 | 126 | Sample Response 127 | 128 | ---------- 129 | Status: 200 OK 130 | { 131 | "email": "thardy@tendrl.org", 132 | "username": "thardy", 133 | "name": "Tom Hardy", 134 | "role": "admin", 135 | "email_notifications": true 136 | } 137 | ---------- 138 | 139 | == Current User 140 | 141 | Retrieve the current logged in user details 142 | 143 | Sample Request 144 | 145 | ---------- 146 | curl -H 'Authorization: Bearer 147 | d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8' -XGET 148 | http://127.0.0.1/api/1.0/current_user 149 | ---------- 150 | 151 | Sample Response 152 | 153 | ---------- 154 | Status: 200 OK 155 | { 156 | "email": "thardy@tendrl.org", 157 | "username": "thardy", 158 | "name": "Tom Hardy", 159 | "role": "admin", 160 | "email_notifications": true 161 | } 162 | ---------- 163 | -------------------------------------------------------------------------------- /docs/volumes.adoc: -------------------------------------------------------------------------------- 1 | // vim: tw=79 2 | = Gluster Volume 3 | :toc: 4 | 5 | == List Volumes 6 | 7 | Sample Request 8 | 9 | ---------- 10 | curl -XGET -H "Authorization: Bearer 11 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" 12 | http://127.0.0.1/api/1.0/clusters/5291c055-70d3-4450-9769-2f6fd4932afb/volumes 13 | ---------- 14 | 15 | Sample Response 16 | 17 | ---------- 18 | Status: 200 OK 19 | { 20 | "volumes": [{ 21 | "client_count": "0", 22 | "redundancy_count": "0", 23 | "snap_count": "0", 24 | "state": "up", 25 | "snapd_status": "Offline", 26 | "name": "vol1", 27 | "replica_count": "1", 28 | "rebalancedetails": { 29 | "66898e03-4db2-4898-b21e-6f034ba10077": { 30 | "rebal_skipped": "0", 31 | "rebal_failures": "0", 32 | "time_left": "", 33 | "rebal_data": "0Bytes", 34 | "rebal_lookedup": "0", 35 | "rebal_status": "not_started", 36 | "hash": "96c33e389a9a875d131fbbfcc031519c", 37 | "updated_at": "2017-09-11 14:07:57.129559+00:00", 38 | "rebal_id": "00000000-0000-0000-0000-000000000000", 39 | "vol_id": "2f99fd35-957b-4fe5-b13c-0255722a8c80", 40 | "rebal_files": "0" 41 | }, 42 | "2f8dcb72-43d8-4ee6-8a12-dfa93a8edbf3": { 43 | "rebal_files": "0", 44 | "rebal_skipped": "0", 45 | "rebal_failures": "0", 46 | "updated_at": "2017-09-11 14:08:05.348291+00:00", 47 | "rebal_data": "0Bytes", 48 | "rebal_id": "00000000-0000-0000-0000-000000000000", 49 | "vol_id": "2f99fd35-957b-4fe5-b13c-0255722a8c80", 50 | "time_left": "", 51 | "rebal_lookedup": "0", 52 | "rebal_status": "not_started", 53 | "hash": "96c33e389a9a875d131fbbfcc031519c" 54 | }, 55 | "35b27276-9058-4c09-b64b-9d239ed262cb": { 56 | "vol_id": "2f99fd35-957b-4fe5-b13c-0255722a8c80", 57 | "rebal_status": "not_started", 58 | "hash": "96c33e389a9a875d131fbbfcc031519c", 59 | "rebal_skipped": "0", 60 | "time_left": "", 61 | "updated_at": "2017-09-12 05:46:55.028369+00:00", 62 | "rebal_id": "00000000-0000-0000-0000-000000000000", 63 | "rebal_lookedup": "0", 64 | "rebal_failures": "0", 65 | "rebal_data": "0Bytes", 66 | "rebal_files": "0" 67 | } 68 | }, 69 | "rebal_estimated_time": "0", 70 | "used_capacity": "4379144192", 71 | "arbiter_count": "0", 72 | "updated_at": "2017-09-12 05:58:02.994041+00:00", 73 | "brick_count": "3", 74 | "usable_capacity": "40221278208", 75 | "deleted": "", 76 | "pcnt_used": "10.8876305953", 77 | "stripe_count": "1", 78 | "disperse_count": "0", 79 | "rebal_status": "not_started", 80 | "quorum_status": "not_applicable", 81 | "vol_type": "Distribute", 82 | "status": "Started", 83 | "vol_id": "2f99fd35-957b-4fe5-b13c-0255722a8c80", 84 | "snapd_inited": "True", 85 | "hash": "64ee6d999dfe2ec298b9883a3c7f904e", 86 | "profiling_enabled": "", 87 | "subvol_count": "3", 88 | "transport_type": "tcp" 89 | }, { 90 | "hash": "2334a3546a9d8b414c36d11e82856ca4", 91 | "rebal_estimated_time": "0", 92 | "client_count": "0", 93 | "status": "Created", 94 | "deleted": "", 95 | "subvol_count": "1", 96 | "replica_count": "1", 97 | "vol_type": "Distribute", 98 | "used_capacity": "1528754176", 99 | "disperse_count": "0", 100 | "pcnt_used": "11.4025777706", 101 | "name": "vol2", 102 | "updated_at": "2017-09-12 05:58:03.511590+00:00", 103 | "snap_count": "0", 104 | "quorum_status": "not_applicable", 105 | "vol_id": "11249c24-e09c-45ac-a1ad-1a36e289e9e1", 106 | "usable_capacity": "13407092736", 107 | "arbiter_count": "0", 108 | "state": "down", 109 | "snapd_status": "Offline", 110 | "transport_type": "tcp", 111 | "snapd_inited": "False", 112 | "brick_count": "1", 113 | "stripe_count": "1", 114 | "profiling_enabled": "", 115 | "redundancy_count": "0", 116 | "rebalancedetails": { 117 | "35b27276-9058-4c09-b64b-9d239ed262cb": { 118 | "updated_at": "2017-09-12 05:47:01.551187+00:00", 119 | "rebal_status": "not_started", 120 | "rebal_files": "0", 121 | "rebal_failures": "0", 122 | "time_left": "", 123 | "rebal_id": "00000000-0000-0000-0000-000000000000", 124 | "rebal_lookedup": "0", 125 | "vol_id": "11249c24-e09c-45ac-a1ad-1a36e289e9e1", 126 | "hash": "c935c806aac9285c492ad8e762b3bd58", 127 | "rebal_skipped": "0", 128 | "rebal_data": "0Bytes" 129 | }, 130 | "66898e03-4db2-4898-b21e-6f034ba10077": { 131 | "vol_id": "11249c24-e09c-45ac-a1ad-1a36e289e9e1", 132 | "hash": "c935c806aac9285c492ad8e762b3bd58", 133 | "rebal_skipped": "0", 134 | "rebal_failures": "0", 135 | "rebal_id": "00000000-0000-0000-0000-000000000000", 136 | "rebal_lookedup": "0", 137 | "rebal_files": "0", 138 | "time_left": "", 139 | "updated_at": "2017-09-11 14:08:01.618013+00:00", 140 | "rebal_data": "0Bytes", 141 | "rebal_status": "not_started" 142 | }, 143 | "2f8dcb72-43d8-4ee6-8a12-dfa93a8edbf3": { 144 | "rebal_skipped": "0", 145 | "rebal_failures": "0", 146 | "rebal_lookedup": "0", 147 | "rebal_status": "not_started", 148 | "hash": "c935c806aac9285c492ad8e762b3bd58", 149 | "time_left": "", 150 | "updated_at": "2017-09-11 14:08:09.995976+00:00", 151 | "rebal_data": "0Bytes", 152 | "rebal_id": "00000000-0000-0000-0000-000000000000", 153 | "vol_id": "11249c24-e09c-45ac-a1ad-1a36e289e9e1", 154 | "rebal_files": "0" 155 | } 156 | }, 157 | "rebal_status": "not_started" 158 | }, { 159 | "replica_count": "1", 160 | "snap_count": "0", 161 | "arbiter_count": "0", 162 | "snapd_status": "Offline", 163 | "snapd_inited": "False", 164 | "profiling_enabled": "", 165 | "vol_type": "Distribute", 166 | "quorum_status": "not_applicable", 167 | "state": "down", 168 | "used_capacity": "2714722304", 169 | "disperse_count": "0", 170 | "hash": "2445c7cb461194f15c77dfd0f6b59541", 171 | "rebalancedetails": { 172 | "35b27276-9058-4c09-b64b-9d239ed262cb": { 173 | "vol_id": "3ea3d010-c6ca-41f5-ab5e-9c244e244a4e", 174 | "rebal_status": "not_started", 175 | "hash": "782ac265a0f813976795262e6560d483", 176 | "rebal_files": "0", 177 | "rebal_failures": "0", 178 | "time_left": "", 179 | "rebal_data": "0Bytes", 180 | "rebal_id": "00000000-0000-0000-0000-000000000000", 181 | "rebal_lookedup": "0", 182 | "rebal_skipped": "0", 183 | "updated_at": "2017-09-12 05:47:14.171856+00:00" 184 | }, 185 | "66898e03-4db2-4898-b21e-6f034ba10077": { 186 | "rebal_files": "0", 187 | "rebal_data": "0Bytes", 188 | "rebal_lookedup": "0", 189 | "rebal_status": "not_started", 190 | "updated_at": "2017-09-11 14:08:26.234576+00:00", 191 | "rebal_id": "00000000-0000-0000-0000-000000000000", 192 | "vol_id": "3ea3d010-c6ca-41f5-ab5e-9c244e244a4e", 193 | "hash": "782ac265a0f813976795262e6560d483", 194 | "rebal_skipped": "0", 195 | "rebal_failures": "0", 196 | "time_left": "" 197 | }, 198 | "2f8dcb72-43d8-4ee6-8a12-dfa93a8edbf3": { 199 | "rebal_failures": "0", 200 | "vol_id": "3ea3d010-c6ca-41f5-ab5e-9c244e244a4e", 201 | "rebal_status": "not_started", 202 | "hash": "782ac265a0f813976795262e6560d483", 203 | "rebal_skipped": "0", 204 | "time_left": "", 205 | "updated_at": "2017-09-11 14:08:37.197984+00:00", 206 | "rebal_data": "0Bytes", 207 | "rebal_id": "00000000-0000-0000-0000-000000000000", 208 | "rebal_lookedup": "0", 209 | "rebal_files": "0" 210 | } 211 | }, 212 | "rebal_estimated_time": "0", 213 | "stripe_count": "1", 214 | "subvol_count": "2", 215 | "pcnt_used": "10.1242020081", 216 | "name": "vol3", 217 | "transport_type": "tcp", 218 | "client_count": "0", 219 | "updated_at": "2017-09-12 05:58:02.307921+00:00", 220 | "usable_capacity": "26814185472", 221 | "redundancy_count": "0", 222 | "brick_count": "2", 223 | "deleted": "", 224 | "rebal_status": "not_started", 225 | "vol_id": "3ea3d010-c6ca-41f5-ab5e-9c244e244a4e", 226 | "status": "Created" 227 | }] 228 | } 229 | ---------- 230 | 231 | == List Bricks per Volume 232 | 233 | Sample Request 234 | 235 | ---------- 236 | curl -XGET -H "Authorization: Bearer 237 | 4b1b225d84104405b52a5646c997c22882aaeba094330c375cb7b0278e9d642a" 238 | http://127.0.0.1/api/1.0/clusters/5291c055-70d3-4450-9769-2f6fd4932afb/volumes/2f99fd35-957b-4fe5-b13c-0255722a8c80/bricks 239 | ---------- 240 | 241 | Sample Response 242 | 243 | ---------- 244 | Status: 200 OK 245 | { 246 | "bricks": [{ 247 | "stripe_size": "", 248 | "hostname": "dhcp-2.lab.tendrl", 249 | "fqdn": "dhcp-2.lab.tendrl", 250 | "vg": "cl_dhcp-2", 251 | "node_id": "2f8dcb72-43d8-4ee6-8a12-dfa93a8edbf3", 252 | "utilization": { 253 | "metadata_used": null, 254 | "used_percent": 11.082067300172056, 255 | "thinpool_used_percent": null, 256 | "used": 1485783040, 257 | "free_inode": 6513157, 258 | "used_inode": 38395, 259 | "used_percent_inode": 0.5860443449124659, 260 | "free": 11921309696, 261 | "total_inode": 6551552, 262 | "mount_point": "/", 263 | "metadata_used_percent": null, 264 | "metadata_free": null, 265 | "thinpool_used": null, 266 | "total": 13407092736, 267 | "thinpool_size": null, 268 | "thinpool_free": null, 269 | "metadata_size": null 270 | }, 271 | "lv": "cl_dhcp42-78-root", 272 | "brick_path": "dhcp-2.lab.tendrl:/root/gluster_bricks/vol1_b3", 273 | "hash": "59b5b8eceab854c5a2631f6f93f4b60c", 274 | "sequence_number": "3", 275 | "updated_at": "2017-09-12 05:16:01.260552+00:00", 276 | "is_arbiter": "", 277 | "status": "Started", 278 | "used": "True", 279 | "name": "dhcp-2.lab.tendrl:_root_gluster_bricks_vol1_b3", 280 | "devices": [], 281 | "pv": [], 282 | "disk_count": "", 283 | "mount_path": "/", 284 | "filesystem_type": "", 285 | "client_count": "", 286 | "size": "13417578496", 287 | "mount_opts": "", 288 | "vol_name": "vol1", 289 | "vol_id": "2f99fd35-957b-4fe5-b13c-0255722a8c80", 290 | "port": "49152", 291 | "disk_type": "", 292 | "brick_dir": "root_gluster_bricks_vol1_b3", 293 | "pool": "", 294 | "brick_id": "root_gluster_bricks_vol1_b3", 295 | "subvolume": "subvolume0" 296 | }, { 297 | "size": "13417578496", 298 | "mount_opts": "", 299 | "updated_at": "2017-09-12 05:16:05.954257+00:00", 300 | "disk_type": "", 301 | "disk_count": "", 302 | "hostname": "dhcp-1.lab.tendrl", 303 | "vol_id": "2f99fd35-957b-4fe5-b13c-0255722a8c80", 304 | "port": "49152", 305 | "is_arbiter": "", 306 | "client_count": "", 307 | "used": "True", 308 | "hash": "407555d2356a6e3fab78997110825919", 309 | "pool": "", 310 | "brick_dir": "root_gluster_bricks_vol1_b2", 311 | "vg": "cl_dhcp-1", 312 | "stripe_size": "", 313 | "pv": [], 314 | "node_id": "66898e03-4db2-4898-b21e-6f034ba10077", 315 | "vol_name": "vol1", 316 | "brick_path": "dhcp-1.lab.tendrl:/root/gluster_bricks/vol1_b2", 317 | "devices": [], 318 | "sequence_number": "2", 319 | "filesystem_type": "", 320 | "utilization": { 321 | "metadata_used": null, 322 | "used_percent": 11.217499853355235, 323 | "thinpool_used_percent": null, 324 | "used": 1503940608, 325 | "free_inode": 6513149, 326 | "used_inode": 38403, 327 | "used_percent_inode": 0.5861664533838677, 328 | "free": 11903152128, 329 | "total_inode": 6551552, 330 | "mount_point": "/", 331 | "metadata_used_percent": null, 332 | "metadata_free": null, 333 | "thinpool_used": null, 334 | "total": 13407092736, 335 | "thinpool_size": null, 336 | "thinpool_free": null, 337 | "metadata_size": null 338 | }, 339 | "lv": "cl_dhcp43-148-root", 340 | "status": "Started", 341 | "name": "dhcp-1.lab.tendrl:_root_gluster_bricks_vol1_b2", 342 | "mount_path": "/", 343 | "fqdn": "dhcp-1.lab.tendrl", 344 | "brick_id": "root_gluster_bricks_vol1_b2" 345 | "subvolume": "subvolume1" 346 | }] 347 | } 348 | ---------- 349 | -------------------------------------------------------------------------------- /firewalld/tendrl-api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | tendrl-api 4 | Enable Port for Tendrl API 5 | 6 | 7 | -------------------------------------------------------------------------------- /lib/tendrl.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'securerandom' 3 | require 'bundler' 4 | 5 | ENV['RACK_ENV'] ||= 'development' 6 | 7 | if File.exist?('.deploy') 8 | require 'sinatra/base' 9 | require 'etcd' 10 | require 'active_model' 11 | require 'active_support' 12 | require 'bcrypt' 13 | else 14 | Bundler.require :default, ENV['RACK_ENV'] 15 | end 16 | 17 | require 'active_support/core_ext/hash' 18 | require 'active_support/inflector' 19 | 20 | # Global Tendrl module 21 | module Tendrl 22 | class << self 23 | def current_definitions 24 | @cluster_definitions || @node_definitions 25 | end 26 | 27 | def cluster_definitions 28 | @cluster_definitions 29 | end 30 | 31 | def cluster_definitions=(definitions) 32 | @node_definitions = nil 33 | @cluster_definitions = definitions 34 | end 35 | 36 | def node_definitions 37 | @node_definitions 38 | end 39 | 40 | def node_definitions=(definitions) 41 | @cluster_definitions = nil 42 | @node_definitions = definitions 43 | end 44 | 45 | def load_node_definitions 46 | defs = etcd.cached_get('/_NS/node_agent/compiled_definitions/data').value 47 | self.node_definitions = YAML.load JSON.parse(defs)['data'] 48 | rescue Etcd::KeyNotFound => e 49 | raise Tendrl::HttpResponseErrorHandler.new( 50 | e, cause: '/_tendrl/definitions' 51 | ) 52 | end 53 | 54 | def load_definitions(cluster_id) 55 | defs = etcd.cached_get("/clusters/#{cluster_id}/_NS/definitions/data").value 56 | self.cluster_definitions = YAML.load JSON.parse(defs)['data'] 57 | rescue Etcd::KeyNotFound => e 58 | raise Tendrl::HttpResponseErrorHandler.new( 59 | e, cause: '/clusters/definitions', object_id: cluster_id 60 | ) 61 | end 62 | 63 | def etcd_config=(config) 64 | $etcd_config = config.freeze 65 | end 66 | 67 | def etcd_config 68 | $etcd_config 69 | end 70 | 71 | def etcd=(etcd_client) 72 | $etcd_client = etcd_client.freeze 73 | end 74 | 75 | def etcd 76 | $etcd_client 77 | end 78 | 79 | def load_config(env) 80 | if File.exist?('/etc/tendrl/etcd.yml') 81 | YAML.load_file('/etc/tendrl/etcd.yml')[env.to_sym] 82 | elsif File.exists?('config/etcd.yml') 83 | YAML.load_file('config/etcd.yml')[env.to_sym] || {} 84 | else 85 | {} 86 | end 87 | end 88 | 89 | def load_cert_config(config) 90 | { 91 | use_ssl: true, 92 | ca_file: config[:ca_cert_file], 93 | ssl_cert: load_client_cert(config[:client_cert_file]), 94 | ssl_key: load_client_key(config[:client_key_file], config[:passphrase]) 95 | } 96 | end 97 | 98 | def load_client_cert(path) 99 | OpenSSL::X509::Certificate.new(File.read(path)) 100 | end 101 | 102 | def load_client_key(path, passphrase) 103 | OpenSSL::PKey::RSA.new(File.read(path), passphrase) 104 | end 105 | 106 | def recurse(parent, attrs = {}, options = {}) 107 | downcase_keys = options[:downcase_keys].nil? || options[:downcase_keys] 108 | parent_key = parent.key.split('/')[-1] 109 | parent_key = parent_key.downcase if downcase_keys 110 | return attrs if ['definitions', 'raw_map'].include?(parent_key) 111 | parent.children.each do |child| 112 | child_key = child.key.split('/')[-1] 113 | child_key = child_key.downcase if downcase_keys 114 | attrs[parent_key] ||= {} 115 | if child.dir 116 | recurse(child, attrs[parent_key], options) 117 | else 118 | attrs[parent_key][child_key] = child.value 119 | unmarshall! attrs[parent_key] 120 | end 121 | end 122 | attrs 123 | end 124 | 125 | def unmarshall!(attrs) 126 | # data can be our serialized data, or a 'data' attribute which is not json 127 | data_value = attrs.delete 'data' 128 | data = JSON.parse(data_value) rescue { 'data' => data_value } 129 | attrs.merge! data if data_value 130 | attrs 131 | end 132 | end 133 | end 134 | 135 | # Initializers 136 | require './config/initializers/etcd' 137 | 138 | # Tendrl core 139 | require './lib/tendrl/version' 140 | require './lib/tendrl/flow' 141 | require './lib/tendrl/object' 142 | require './lib/tendrl/atom' 143 | require './lib/tendrl/attribute' 144 | require './lib/tendrl/http_response_error_handler' 145 | 146 | # Models 147 | require './app/models/user' 148 | require './app/models/node' 149 | require './app/models/cluster' 150 | require './app/models/alert' 151 | require './app/models/notification' 152 | require './app/models/job' 153 | require './app/models/volume' 154 | require './app/models/brick' 155 | 156 | # Forms 157 | require './app/forms/user_form' 158 | 159 | # Presenters 160 | require './app/presenters/node_presenter' 161 | require './app/presenters/cluster_presenter' 162 | require './app/presenters/job_presenter' 163 | require './app/presenters/user_presenter' 164 | require './app/presenters/volume_presenter' 165 | require './app/presenters/brick_presenter' 166 | require './app/presenters/notification_presenter' 167 | 168 | # Errors 169 | require './lib/tendrl/errors/tendrl_error' 170 | require './lib/tendrl/errors/invalid_object_error' 171 | 172 | # Contollers 173 | require './app/controllers/application_controller' 174 | require './app/controllers/ping_controller' 175 | require './app/controllers/authenticated_users_controller' 176 | require './app/controllers/nodes_controller' 177 | require './app/controllers/clusters_controller' 178 | require './app/controllers/jobs_controller' 179 | require './app/controllers/users_controller' 180 | require './app/controllers/sessions_controller' 181 | require './app/controllers/alerting_controller' 182 | require './app/controllers/notifications_controller' 183 | -------------------------------------------------------------------------------- /lib/tendrl/atom.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Atom 3 | def initialize(type, values) 4 | @type = type 5 | @values = values 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tendrl/attribute.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Attribute 3 | attr_reader :name, :help, :type, :default, :required 4 | 5 | def initialize(object_type, name, attributes) 6 | attributes ||= {} 7 | @object_type = object_type 8 | @name = name 9 | @help = attributes['help'] 10 | @type = attributes['type'] 11 | @default = attributes['default'] 12 | @required = nil 13 | end 14 | 15 | def to_hash 16 | { 17 | name: "#{@object_type}.#{@name}", 18 | help: @help, 19 | type: @type, 20 | default: @default, 21 | required: @required 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/tendrl/errors/invalid_object_error.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class InvalidObjectError < TendrlError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/tendrl/errors/tendrl_error.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class TendrlError < StandardError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/tendrl/flow.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Flow 3 | METHOD_MAPPING = { 4 | 'create' => 'POST', 5 | 'update' => 'PUT', 6 | 'delete' => 'DELETE', 7 | 'action' => 'GET' 8 | }.freeze 9 | 10 | attr_reader :namespace, :flow_name 11 | 12 | def initialize(namespace, flow_name, object = nil) 13 | @instance = Tendrl.current_definitions 14 | @namespace = namespace 15 | @flow_name = flow_name 16 | @object = object 17 | @objects = @instance[namespace]['objects'] 18 | flows = @instance[namespace]['flows'] || {} 19 | @flow = flows[flow_name] || @objects[object]['flows'][flow_name] 20 | end 21 | 22 | def objects 23 | @objects.keys.map do |object_name| 24 | Object.new(namespace, object_name) 25 | end 26 | end 27 | 28 | def sds_name 29 | if @namespace.end_with?('gluster') 30 | 'gluster' 31 | elsif @namespace.end_with?('ceph') 32 | 'ceph' 33 | end 34 | end 35 | 36 | def name 37 | "#{sds_name.to_s.capitalize}#{@flow_name}" 38 | end 39 | 40 | def tags(context) 41 | tags = [] 42 | return tags unless @flow['tags'] 43 | @flow['tags'].each do |tag| 44 | finalized_tag = [] 45 | placeholders = tag.split('/') 46 | placeholders.each do |placeholder| 47 | finalized_tag << if placeholder.start_with?('$') 48 | context[placeholder[1..-1]] 49 | else 50 | placeholder 51 | end 52 | end 53 | tags << finalized_tag.join('/') 54 | end 55 | tags 56 | end 57 | 58 | def reference_attributes 59 | attributes = [] 60 | objects.each do |obj| 61 | attributes << obj.attributes 62 | end 63 | attributes 64 | end 65 | 66 | def attributes 67 | mandatory_attributes + optional_attributes 68 | end 69 | 70 | def type 71 | @flow['type'].downcase 72 | end 73 | 74 | def run 75 | @flow['run'] 76 | end 77 | 78 | def method 79 | METHOD_MAPPING[type] || 'POST' 80 | end 81 | 82 | def mandatory_attributes 83 | flow_attributes = [] 84 | mandatory_attributes = @flow['inputs']['mandatory'] || [] 85 | mandatory_attributes.each do |ma| 86 | next if ma.end_with?('cluster_id') 87 | if ma.end_with?('[]') 88 | flow_attributes << { name: ma, type: 'List', 89 | required: true } 90 | else 91 | attribute = Object.find_by_attribute(ma) 92 | attribute[:required] = true 93 | flow_attributes << attribute 94 | end 95 | end 96 | flow_attributes 97 | end 98 | 99 | def optional_attributes 100 | flow_attributes = [] 101 | optional_attributes = @flow['inputs']['optional'] || [] 102 | optional_attributes.each do |ma| 103 | next if ma.end_with?('cluster_id') 104 | if ma.end_with?('[]') 105 | flow_attributes << { 106 | name: ma, 107 | type: 'List', 108 | required: false 109 | } 110 | else 111 | attribute = Object.find_by_attribute(ma) 112 | attribute[:required] = false 113 | flow_attributes << attribute 114 | end 115 | end 116 | flow_attributes 117 | end 118 | 119 | def self.find_all 120 | flows = [] 121 | Tendrl.current_definitions.keys.map do |key| 122 | next unless [ 123 | 'namespace.tendrl', 124 | 'namespace.gluster', 125 | 'namespace.ceph', 126 | 'namespace.node_agent' 127 | ].include?(key) 128 | if Tendrl.current_definitions[key]['flows'] 129 | Tendrl.current_definitions[key]['flows'].keys.each do |fk| 130 | flow = Tendrl::Flow.new(key, fk) 131 | flows << { 132 | name: flow.name, 133 | method: flow.method, 134 | attributes: flow.attributes 135 | } 136 | end 137 | end 138 | Tendrl.current_definitions[key]['objects'].keys.each do |ok| 139 | object_flows = Tendrl.current_definitions[key]['objects'][ok]['flows'] 140 | next if object_flows.nil? 141 | object_flows.keys.each do |fk| 142 | flow = Tendrl::Flow.new(key, fk, ok) 143 | flows << { 144 | name: flow.name, 145 | method: flow.method, 146 | attributes: flow.attributes 147 | } 148 | end 149 | end 150 | end 151 | flows 152 | end 153 | 154 | def self.find_by_external_name_and_type(external_name, type) 155 | if type == 'node' 156 | namespace = 'namespace.tendrl' 157 | flow = external_name 158 | elsif type == 'cluster' 159 | partial_namespace = 'namespace' 160 | sds_parameters = external_name.underscore.split('_') 161 | namespace = "#{partial_namespace}.#{sds_parameters[0].downcase}" 162 | flow = sds_parameters[1..-1].join('_').to_s.camelize 163 | object = sds_parameters[2].capitalize 164 | elsif type == 'node_agent' 165 | namespace = 'namespace.node_agent' 166 | flow = external_name 167 | end 168 | new(namespace, flow, object) 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/tendrl/http_response_error_handler.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class HttpResponseErrorHandler < StandardError 3 | attr_reader :status, :body 4 | 5 | EXCEPTION_MAPPING = { 6 | '/_tendrl/definitions' => { 7 | body: { 8 | errors: { 9 | message: 'Node definitions file not found.' 10 | } 11 | }, 12 | status: 404 13 | }, 14 | '/clusters' => { 15 | status: 200, 16 | body: { clusters: [] } 17 | }, 18 | '/queue' => { 19 | status: 200, 20 | body: { jobs: [] } 21 | }, 22 | '/clusters/definitions' => { 23 | status: 404, 24 | body: { 25 | errors: { 26 | message: 'Cluster definitions for cluster_id %s not found.' 27 | } 28 | } 29 | }, 30 | '/clusters/id' => { 31 | status: 404, 32 | body: { 33 | errors: { 34 | message: 'Cluster with cluster_id %s not found.' 35 | } 36 | } 37 | }, 38 | '/jobs/id' => { 39 | status: 404, 40 | body: { 41 | errors: { 42 | message: 'Job with job_id %s not found.' 43 | } 44 | } 45 | }, 46 | 'invalid_json' => { 47 | status: 400, 48 | body: { 49 | errors: { 50 | message: 'Invalid JSON received.' 51 | } 52 | } 53 | }, 54 | 'etcd_timeout' => { 55 | status: 408, 56 | body: { 57 | errors: { 58 | message: 'Service timeout' 59 | } 60 | } 61 | }, 62 | 'etcd_not_reachable' => { 63 | status: 503, 64 | body: { 65 | errors: { 66 | message: 'Service unavailable' 67 | } 68 | } 69 | }, 70 | 'uncaught_exception' => { 71 | status: 500, 72 | body: { 73 | errors: { 74 | message: 'Internal server error' 75 | } 76 | } 77 | } 78 | }.freeze 79 | 80 | def initialize(error, cause: nil, object_id: nil) 81 | @error = error 82 | @cause = cause 83 | @object_id = object_id 84 | @mapping = EXCEPTION_MAPPING[cause] || default_mapping 85 | @body = @mapping[:body] 86 | if @object_id 87 | @body[:errors][:message] = @body[:errors][:message] % [@object_id] 88 | end 89 | @status = @mapping[:status] 90 | end 91 | 92 | def default_mapping 93 | { 94 | body: { 95 | errors: { 96 | message: @error.message.to_s 97 | } 98 | }, 99 | status: 404 100 | } 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/tendrl/object.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | class Object 3 | def initialize(namespace, type) 4 | @config = Tendrl.current_definitions 5 | @type = type 6 | @namespace = namespace 7 | @object = @config[namespace]['objects'][type] 8 | end 9 | 10 | def attributes 11 | @object['attrs'].map do |attr_name, values| 12 | Attribute.new(@type, attr_name, values) 13 | end 14 | end 15 | 16 | def atoms 17 | @object['atoms'].map do |atom_name, values| 18 | Atom.new(atom_name, values) 19 | end 20 | end 21 | 22 | def self.find_by_object_name(object_name) 23 | object = nil 24 | Tendrl.current_definitions.keys.each do |key| 25 | next if key == 'tendrl_schema_version' 26 | if objects = Tendrl.current_definitions[key]['objects'] 27 | objects = objects.keys 28 | if objects.include?(object_name) 29 | object = Object.new(key, object_name) 30 | break 31 | end 32 | end 33 | end 34 | object 35 | end 36 | 37 | def self.find_by_attribute(attribute) 38 | object_name, attribute = attribute.split('.') 39 | object = find_by_object_name(object_name) 40 | attribute = object.attributes.find{ |a| a.name == attribute } 41 | attribute.to_hash 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/tendrl/version.rb: -------------------------------------------------------------------------------- 1 | module Tendrl 2 | VERSION = '0.0.2'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /public/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tendrl/api/b7ce23e515e5359fc5198ad303b677cd1b6dad69/public/.keep -------------------------------------------------------------------------------- /spec/controllers/authenticated_users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require './app/controllers/authenticated_users_controller' 3 | 4 | RSpec.describe AuthenticatedUsersController do 5 | 6 | let(:http_env){ 7 | { 8 | 'HTTP_AUTHORIZATION' => 'Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8', 9 | 'CONTENT_TYPE' => 'application/json' 10 | } 11 | } 12 | 13 | before do 14 | stub_user('dwarner') 15 | stub_access_token('dwarner') 16 | end 17 | 18 | context 'users' do 19 | 20 | it 'current' do 21 | get "/current_user",{}, http_env 22 | expect(last_response.status).to eq 200 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/controllers/clusters_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require './app/controllers/clusters_controller' 3 | 4 | describe ClustersController do 5 | 6 | let(:http_env){ 7 | { 8 | 'HTTP_AUTHORIZATION' => 'Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8', 9 | 'CONTENT_TYPE' => 'application/json' 10 | } 11 | } 12 | 13 | before do 14 | stub_user('dwarner') 15 | stub_access_token('dwarner') 16 | end 17 | 18 | context 'list' do 19 | 20 | before do 21 | stub_clusters 22 | end 23 | 24 | it 'clusters' do 25 | get "/clusters", {}, http_env 26 | expect(last_response.status).to eq 200 27 | end 28 | 29 | end 30 | 31 | context 'show' do 32 | before do 33 | stub_cluster 34 | end 35 | 36 | specify 'cluster details' do 37 | get '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc', nil, http_env 38 | expect(last_response.status).to eq 200 39 | body = JSON.parse last_response.body 40 | expect(body['short_name']).to eq('cluster-short-name') 41 | end 42 | end 43 | 44 | context 'import' do 45 | before do 46 | stub_definitions 47 | end 48 | 49 | it 'unmanaged' do 50 | stub_unmanaged_cluster 51 | stub_job_creation 52 | body = { 53 | 'Cluster.volume_profiling_flag' => "enable" 54 | } 55 | post '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/import', 56 | body.to_json, 57 | http_env 58 | expect(last_response.status).to eq 202 59 | end 60 | 61 | specify 'unknown cluster' do 62 | stub_unknown_cluster 63 | post '/clusters/unknown/import', '{}', http_env 64 | expect(last_response.status).to eq 404 65 | end 66 | end 67 | 68 | context 'expand' do 69 | before do 70 | stub_definitions 71 | stub_unmanaged_cluster 72 | stub_job_creation 73 | end 74 | 75 | specify 'cluster with detected peers' do 76 | post '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/expand', 77 | nil, http_env 78 | expect(last_response.status).to eq 202 79 | end 80 | end 81 | 82 | context 'unmanage' do 83 | 84 | before do 85 | stub_definitions 86 | stub_managed_cluster 87 | stub_job_creation 88 | end 89 | 90 | it 'managed' do 91 | post '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/unmanage', 92 | '{}', http_env 93 | expect(last_response.status).to eq 202 94 | 95 | post '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/unmanage', 96 | nil, http_env 97 | expect(last_response.status).to eq 202 98 | end 99 | 100 | end 101 | 102 | context 'profiling' do 103 | 104 | before do 105 | stub_cluster 106 | stub_cluster_profiling 107 | stub_cluster_definitions 108 | stub_job_creation 109 | end 110 | 111 | it 'enable' do 112 | body = { 113 | 'Cluster.volume_profiling_flag' => 'enable' 114 | } 115 | post '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/profiling', 116 | body.to_json, 117 | http_env 118 | expect(last_response.status).to eq 202 119 | end 120 | 121 | specify 'start_profiling' do 122 | post '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/volumes/3199b12f-0626-44ea-9315-03614a595e90/start_profiling', 123 | nil, http_env 124 | expect(last_response.status).to eq 202 125 | end 126 | 127 | specify 'stop_profiling' do 128 | post '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/volumes/3199b12f-0626-44ea-9315-03614a595e90/stop_profiling', 129 | nil, http_env 130 | expect(last_response.status).to eq 202 131 | end 132 | end 133 | 134 | context 'actions' do 135 | 136 | before do 137 | stub_cluster_context 138 | stub_job_creation 139 | end 140 | 141 | context 'unknown object' do 142 | 143 | before do 144 | stub_cluster_definitions('ceph') 145 | stub_node_ids 146 | end 147 | 148 | it 'list' do 149 | get '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/GetNodeList', 150 | {}, 151 | http_env 152 | expect(last_response.status).to eq(404) 153 | end 154 | 155 | end 156 | 157 | context 'volumes' do 158 | 159 | before do 160 | stub_cluster_definitions('gluster') 161 | stub_volumes 162 | stub_node_ids 163 | end 164 | 165 | it 'list' do 166 | get '/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/volumes', {}, 167 | http_env 168 | expect(last_response.status).to eq 200 169 | end 170 | 171 | end 172 | 173 | context 'bricks' do 174 | 175 | end 176 | 177 | end 178 | end 179 | 180 | -------------------------------------------------------------------------------- /spec/controllers/jobs_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require './app/controllers/jobs_controller' 3 | 4 | describe JobsController do 5 | 6 | let(:http_env){ 7 | { 8 | 'HTTP_AUTHORIZATION' => 'Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8', 9 | 'CONTENT_TYPE' => 'application/json' 10 | } 11 | } 12 | 13 | before do 14 | stub_user('dwarner') 15 | stub_access_token('dwarner') 16 | end 17 | 18 | context 'jobs' do 19 | 20 | it 'list' do 21 | stub_jobs 22 | get '/jobs', {}, http_env 23 | expect(last_response.status).to eq 200 24 | end 25 | 26 | it 'details' do 27 | stub_job 28 | get '/jobs/3d38fb70-6865-4d77-82e0-22509359efef', {}, http_env 29 | expect(last_response.status).to eq 200 30 | end 31 | 32 | specify 'missing job' do 33 | stub_missing_job 34 | get '/jobs/missing', {}, http_env 35 | expect(last_response.status).to eq 404 36 | 37 | get '/jobs/missing/messages', {}, http_env 38 | expect(last_response.status).to eq 404 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/controllers/nodes_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require './app/controllers/nodes_controller' 3 | 4 | describe NodesController do 5 | 6 | let(:http_env){ 7 | { 8 | 'HTTP_AUTHORIZATION' => 'Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8', 9 | 'CONTENT_TYPE' => 'application/json' 10 | } 11 | } 12 | 13 | before do 14 | stub_user('dwarner') 15 | stub_access_token('dwarner') 16 | stub_definitions 17 | end 18 | 19 | context 'create' do 20 | 21 | end 22 | 23 | # context 'list' do 24 | # 25 | # before do 26 | # stub_nodes 27 | # stub_clusters(false) 28 | # end 29 | # 30 | # it 'nodes' do 31 | # get "/nodes", {}, http_env 32 | # expect(last_response.status).to eq 200 33 | # end 34 | # 35 | # end 36 | 37 | context 'node agent' do 38 | 39 | before do 40 | stub_nodes 41 | stub_job_creation 42 | end 43 | 44 | it 'generate journal mapping' do 45 | body = { 46 | "Cluster.node_configuration" => { 47 | "c573b8b8-2488-4db7-8033-27b9a468bce3" => { 48 | "storage_disks" => [ 49 | {"device" => "/dev/vdb", "size" => 189372825600, "ssd" => false}, 50 | {"device" => "/dev/vdc", "size" => 80530636800, "ssd" => false}, 51 | {"device" => "/dev/vdd", "size" => 107374182400, "ssd" => false}, 52 | {"device" => "/dev/vde", "size" => 21474836480, "ssd" => false}, 53 | {"device" => "/dev/vdf", "size" => 26843545600, "ssd" => false } 54 | ] 55 | } 56 | } 57 | } 58 | post '/GenerateJournalMapping', body.to_json, http_env 59 | expect(last_response.status). to eq 202 60 | end 61 | 62 | end 63 | 64 | context 'expand' do 65 | 66 | before do 67 | stub_definitions 68 | stub_job_creation 69 | end 70 | 71 | it 'gluster cluster' do 72 | body = { 73 | 'sds_name' => 'gluster', 74 | 'Cluster.node_configuration' => { 75 | '867d6aae-fb98-4060-9f6a-1da4e4988db8' => { 76 | 'role' => 'glusterfs/node', 77 | 'provisioning_ip' => '0.0.0.0' 78 | } 79 | } 80 | } 81 | put '/6b4b84e0-17b3-4543-af9f-e42000c52bfc/ExpandCluster', body.to_json, http_env 82 | expect(last_response.status).to eq 202 83 | end 84 | 85 | it 'ceph cluster' do 86 | body = { 87 | 'sds_name' => 'ceph', 88 | 'Cluster.node_configuration' => { 89 | '867d6aae-fb98-4060-9f6a-1da4e4988db8' => { 90 | 'role' => 'ceph/mon', 91 | 'provisioning_ip' => '0.0.0.0', 92 | 'monitor_interface' => 'eth0' 93 | }, 94 | "d41d073f-db4e-4abf-8018-02b30254b912" => { 95 | "role" => "ceph/osd", 96 | "provisioning_ip" => "0.0.0.0", 97 | "journal_size" => 5120, 98 | "journal_colocation" => false, 99 | "storage_disks" => [ 100 | {"device" => "/dev/vdb", "journal" => "/dev/vdc"}, 101 | {"device" => "/dev/vdd", "journal" => "/dev/vde"} 102 | ] 103 | } 104 | } 105 | } 106 | put '/6b4b84e0-17b3-4543-af9f-e42000c52bfc/ExpandCluster', body.to_json, http_env 107 | expect(last_response.status).to eq 202 108 | end 109 | end 110 | 111 | 112 | end 113 | 114 | -------------------------------------------------------------------------------- /spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tendrl/api/b7ce23e515e5359fc5198ad303b677cd1b6dad69/spec/controllers/sessions_controller_spec.rb -------------------------------------------------------------------------------- /spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require './app/controllers/users_controller' 3 | 4 | RSpec.describe UsersController do 5 | 6 | context "admin user" do 7 | 8 | let(:http_env){ 9 | { 10 | 'HTTP_AUTHORIZATION' => 'Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8', 11 | 'CONTENT_TYPE' => 'application/json' 12 | } 13 | } 14 | 15 | before do 16 | stub_users 17 | stub_user('dwarner') 18 | stub_access_token('dwarner') 19 | end 20 | 21 | context 'create' do 22 | 23 | let(:body){ 24 | { 25 | email: 'thardy@tendrl.org', 26 | username: 'thardy', 27 | name: 'Tom Hardy', 28 | password: 'temp12345', 29 | role: 'admin', 30 | email_notifications: true 31 | } 32 | } 33 | 34 | before do 35 | stub_user_create(body[:username]) 36 | stub_create_user_attributes(body) 37 | stub_user(body[:username]) 38 | stub_email_notifications_index(body[:username], body[:email]) 39 | end 40 | 41 | it 'invalid attributes' do 42 | post "/users", { username: body[:username] }.to_json, http_env 43 | expect(last_response.status).to eq(422) 44 | end 45 | 46 | it 'valid attributes' do 47 | post "/users", body.to_json, http_env 48 | expect(last_response.status).to eq(201) 49 | end 50 | 51 | end 52 | 53 | specify 'update other user' do 54 | stub_user('quentin') 55 | body = { name: 'Quentin2 D' } 56 | expect(Tendrl::User).to receive(:save).and_return( 57 | Tendrl::User.find('quentin') 58 | ) 59 | put '/users/quentin', body.to_json, http_env 60 | expect(last_response.status).to eq(200) 61 | end 62 | 63 | context 'delete' do 64 | 65 | it 'other non admin user succssfully' do 66 | stub_user('quentin') 67 | stub_delete_user('quentin') 68 | stub_delete_email_notifications_index('quentin') 69 | delete "/users/quentin", {}, http_env 70 | expect(last_response.status).to eq(200) 71 | end 72 | 73 | it 'self unsuccessfully' do 74 | delete "/users/dwarner", {}, http_env 75 | expect(last_response.status).to eq(403) 76 | end 77 | 78 | end 79 | end 80 | 81 | context "normal user" do 82 | 83 | let(:http_env){ 84 | { 85 | 'HTTP_AUTHORIZATION' => 'Bearer d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8', 86 | 'CONTENT_TYPE' => 'application/json' 87 | } 88 | } 89 | 90 | context 'create' do 91 | 92 | before do 93 | stub_user('quentin') 94 | stub_access_token('quentin') 95 | end 96 | 97 | it 'new user is forbidden' do 98 | post "/users", {}.to_json, http_env 99 | expect(last_response.status).to eq(403) 100 | end 101 | end 102 | 103 | context 'update' do 104 | before do 105 | stub_users 106 | stub_user('quentin') 107 | stub_access_token('quentin') 108 | end 109 | 110 | it 'other user is forbidden' do 111 | put '/users/thardy', { name: 'Hardy' }.to_json, http_env 112 | expect(last_response.status).to eq(403) 113 | end 114 | 115 | specify 'self' do 116 | body = { name: 'Quentin2 D', username: 'quentin' } 117 | expect(Tendrl::User).to receive(:save).and_return( 118 | Tendrl::User.find('quentin') 119 | ) 120 | put '/users/quentin', body.to_json, http_env 121 | expect(last_response.status).to eq(200) 122 | end 123 | 124 | specify 'username is not allowed' do 125 | body = { name: 'Quentin2 D', username: 'quentin2' } 126 | put '/users/quentin', body.to_json, http_env 127 | expect(last_response.status).to eq(422) 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/fixtures/cluster_context.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/TendrlContext", 5 | "dir": true, 6 | "nodes": [{ 7 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/TendrlContext/sds_version", 8 | "value": "10.2.5", 9 | "modifiedIndex": 7055, 10 | "createdIndex": 7055 11 | }, { 12 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/TendrlContext/fsid", 13 | "value": "c221ccdb-51d6-4b57-9f10-bcf30c7fa351", 14 | "modifiedIndex": 7051, 15 | "createdIndex": 7051 16 | }, { 17 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/TendrlContext/integration_id", 18 | "value": "6b4b84e0-17b3-4543-af9f-e42000c52bfc", 19 | "modifiedIndex": 7052, 20 | "createdIndex": 7052 21 | }, { 22 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/TendrlContext/name", 23 | "value": "ceph", 24 | "modifiedIndex": 7053, 25 | "createdIndex": 7053 26 | }, { 27 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/TendrlContext/sds_name", 28 | "value": "ceph", 29 | "modifiedIndex": 7054, 30 | "createdIndex": 7054 31 | }], 32 | "modifiedIndex": 3506, 33 | "createdIndex": 3506 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spec/fixtures/create_user.json: -------------------------------------------------------------------------------- 1 | {"action":"set","node":{"key":"/_tendrl/users/anivargi","dir":true,"modifiedIndex":183,"createdIndex":183}} 2 | -------------------------------------------------------------------------------- /spec/fixtures/detected_cluster.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/nodes/3a6cdc47-83b1-49cd-861c-5746066d8fb0/DetectedCluster/detected_cluster_id", 5 | "value": "442a8d12-4790-498c-967c-a6ba4692a54d", 6 | "modifiedIndex": 2173167, 7 | "createdIndex": 2173167 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixtures/dwarner.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/_tendrl/users/dwarner", 5 | "dir": true, 6 | "nodes": [{ 7 | "key": "/_tendrl/users/dwarner/username", 8 | "value": "dwarner", 9 | "modifiedIndex": 191, 10 | "createdIndex": 191 11 | }, { 12 | "key": "/_tendrl/users/dwarner/access_token", 13 | "value": "d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8", 14 | "expiration": "2017-02-23T16:37:30.353662562Z", 15 | "ttl": 511606, 16 | "modifiedIndex": 197, 17 | "createdIndex": 197 18 | }, { 19 | "key": "/_tendrl/users/dwarner/email", 20 | "value": "dwarner@tendrl.org", 21 | "modifiedIndex": 190, 22 | "createdIndex": 190 23 | }, { 24 | "key": "/_tendrl/users/dwarner/name", 25 | "value": "David Warner", 26 | "modifiedIndex": 189, 27 | "createdIndex": 189 28 | }, { 29 | "key": "/_tendrl/users/dwarner/password_hash", 30 | "value": "$2a$10$2TEvrFp5cKo9dFvJSRyp6e7g5GzGzic7X4Xu1n.Cf2WCiiOuh3o.u", 31 | "modifiedIndex": 193, 32 | "createdIndex": 193 33 | }, { 34 | "key": "/_tendrl/users/dwarner/password_salt", 35 | "value": "$2a$10$2TEvrFp5cKo9dFvJSRyp6e", 36 | "modifiedIndex": 192, 37 | "createdIndex": 192 38 | },{ 39 | "key": "/_tendrl/users/dwarner/email_notifications", 40 | "value": "true", 41 | "modifiedIndex": 192, 42 | "createdIndex": 192 43 | },{ 44 | "key": "/_tendrl/users/dwarner/role", 45 | "value": "admin", 46 | "modifiedIndex": 192, 47 | "createdIndex": 192 48 | }], 49 | "modifiedIndex": 188, 50 | "createdIndex": 188 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spec/fixtures/ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/_tendrl/users/dwarner", 5 | "dir": false, 6 | "value": "3a95fd96-876d-439a-a64d-70332c069aaa", 7 | "modifiedIndex": 188, 8 | "createdIndex": 188 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/job.yml: -------------------------------------------------------------------------------- 1 | --- !ruby/object:Etcd::Response 2 | action: get 3 | node: !ruby/object:Etcd::Node 4 | created_index: 946508 5 | modified_index: 946508 6 | ttl: 101105 7 | key: /queue/3d38fb70-6865-4d77-82e0-22509359efef 8 | value: 9 | expiration: '2018-05-18T10:03:11.522478003Z' 10 | dir: true 11 | children: 12 | - !ruby/object:Etcd::Node 13 | created_index: 947059 14 | modified_index: 947059 15 | ttl: 16 | key: /queue/3d38fb70-6865-4d77-82e0-22509359efef/updated_at 17 | value: '2018-05-16 10:03:16.926304+00:00' 18 | expiration: 19 | dir: 20 | - !ruby/object:Etcd::Node 21 | created_index: 947063 22 | modified_index: 947063 23 | ttl: 24 | key: /queue/3d38fb70-6865-4d77-82e0-22509359efef/hash 25 | value: f2f150505415236da5b64c8185d6ad58 26 | expiration: 27 | dir: 28 | - !ruby/object:Etcd::Node 29 | created_index: 947054 30 | modified_index: 947054 31 | ttl: 32 | key: /queue/3d38fb70-6865-4d77-82e0-22509359efef/locked_by 33 | value: '{"node_id": "c0ef7e50-61b7-45d3-ba47-a75168e12a60", "type": "node", "fqdn": 34 | "tendrl-node-1", "tags": ["detected_cluster/9152e40ea91afacbdd65f8008ee4f85ccceed0cc57ebb01074b776493a599430", 35 | "gluster/server", "provisioner/928ef2dd-31ab-4bcf-977b-533ddc96249a", "tendrl/integration/84204743-0a5c-47bc-9e10-214a85395a59", 36 | "tendrl/integration/928ef2dd-31ab-4bcf-977b-533ddc96249a", "tendrl/integration/gluster", 37 | "tendrl/node", "tendrl/node_c0ef7e50-61b7-45d3-ba47-a75168e12a60"]}' 38 | expiration: 39 | dir: 40 | - !ruby/object:Etcd::Node 41 | created_index: 947057 42 | modified_index: 947057 43 | ttl: 44 | key: /queue/3d38fb70-6865-4d77-82e0-22509359efef/data 45 | value: '{"status": "finished", "valid_until": "", "errors": "", "job_id": "3d38fb70-6865-4d77-82e0-22509359efef", 46 | "locked_by": "{\"node_id\": \"c0ef7e50-61b7-45d3-ba47-a75168e12a60\", \"type\": 47 | \"node\", \"fqdn\": \"tendrl-node-1\", \"tags\": [\"detected_cluster/9152e40ea91afacbdd65f8008ee4f85ccceed0cc57ebb01074b776493a599430\", 48 | \"gluster/server\", \"provisioner/928ef2dd-31ab-4bcf-977b-533ddc96249a\", \"tendrl/integration/84204743-0a5c-47bc-9e10-214a85395a59\", 49 | \"tendrl/integration/928ef2dd-31ab-4bcf-977b-533ddc96249a\", \"tendrl/integration/gluster\", 50 | \"tendrl/node\", \"tendrl/node_c0ef7e50-61b7-45d3-ba47-a75168e12a60\"]}", "children": 51 | "[]", "timeout": "yes", "output": {}, "payload": {"status": "new", "run": "tendrl.flows.ConfigureMonitoring", 52 | "type": "node", "parameters": {"TendrlContext.integration_id": "928ef2dd-31ab-4bcf-977b-533ddc96249a"}, 53 | "tags": ["tendrl/node_c0ef7e50-61b7-45d3-ba47-a75168e12a60"]}}' 54 | expiration: 55 | dir: 56 | etcd_index: 1039888 57 | raft_index: 4009591 58 | raft_term: 10 59 | -------------------------------------------------------------------------------- /spec/fixtures/job_created.json: -------------------------------------------------------------------------------- 1 | {"action":"set","node":{"key":"/queue/0f25d7d4-10f9-45b1-a6f0-4850124bc07c","value":"","modifiedIndex":683855,"createdIndex":683855}} 2 | -------------------------------------------------------------------------------- /spec/fixtures/jobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/queue", 5 | "dir": true, 6 | "nodes": [{ 7 | "key": "/queue/dd4e2ec4-5c0a-4248-93b2-1112892bd4bf", 8 | "dir": true, 9 | "nodes": [{ 10 | "key": "/queue/dd4e2ec4-5c0a-4248-93b2-1112892bd4bf/status", 11 | "value": "new", 12 | "modifiedIndex": 343, 13 | "createdIndex": 343 14 | }, { 15 | "key": "/queue/dd4e2ec4-5c0a-4248-93b2-1112892bd4bf/payload", 16 | "value": "{\"job_id\":\"dd4e2ec4-5c0a-4248-93b2-1112892bd4bf\",\"integration_id\":\"360ddf03-dc60-4d56-814d-41e37d678e15\",\"status\":\"new\",\"run\":\"tendrl.node_agent.flows.import_cluster.ImportCluster\",\"flow\":\"ImportCluster\",\"type\":\"node\",\"created_from\":\"API\",\"created_at\":\"2017-03-02T15:19:26Z\",\"username\":\"dwarner\",\"parameters\":{\"node_ids\":[\"3b6eb27f-3e83-4751-9d45-85a989ae2b25\"],\"sds_name\":\"ceph 10.2.5\",\"sds_version\":\"10.2.5\",\"sds_type\":\"ceph\",\"DetectedCluster.sds_pkg_name\":\"ceph\",\"Node[]\":[\"3b6eb27f-3e83-4751-9d45-85a989ae2b25\"],\"TendrlContext.integration_id\":\"360ddf03-dc60-4d56-814d-41e37d678e15\"},\"node_ids\":[\"3b6eb27f-3e83-4751-9d45-85a989ae2b25\"]}", 17 | "modifiedIndex": 344, 18 | "createdIndex": 344 19 | }], 20 | "modifiedIndex": 343, 21 | "createdIndex": 343 22 | }], 23 | "modifiedIndex": 343, 24 | "createdIndex": 343 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/fixtures/node.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4", 5 | "dir": true, 6 | "nodes": [{ 7 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/NodeContext", 8 | "dir": true, 9 | "modifiedIndex": 39, 10 | "createdIndex": 39 11 | }, { 12 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/DetectedCluster", 13 | "dir": true, 14 | "modifiedIndex": 84, 15 | "createdIndex": 84 16 | }, { 17 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/Memory", 18 | "dir": true, 19 | "modifiedIndex": 236, 20 | "createdIndex": 236 21 | }, { 22 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/Disks", 23 | "dir": true, 24 | "modifiedIndex": 605420, 25 | "createdIndex": 605420 26 | }, { 27 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/Cpu", 28 | "dir": true, 29 | "modifiedIndex": 225, 30 | "createdIndex": 225 31 | }, { 32 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/Networks", 33 | "dir": true, 34 | "modifiedIndex": 1018, 35 | "createdIndex": 1018 36 | }, { 37 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/TendrlContext", 38 | "dir": true, 39 | "modifiedIndex": 56, 40 | "createdIndex": 56 41 | }, { 42 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/Platform", 43 | "dir": true, 44 | "modifiedIndex": 77, 45 | "createdIndex": 77 46 | }, { 47 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/Service", 48 | "dir": true, 49 | "modifiedIndex": 97, 50 | "createdIndex": 97 51 | }, { 52 | "key": "/nodes/91e74d1e-93fd-4f1f-b306-762f9fe918b4/Os", 53 | "dir": true, 54 | "modifiedIndex": 207, 55 | "createdIndex": 207 56 | }], 57 | "modifiedIndex": 39, 58 | "createdIndex": 39 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /spec/fixtures/node_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/nodes", 5 | "dir": true, 6 | "nodes": [{ 7 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/nodes/3413aaeb-9103-4586-b8c8-093e96480452", 8 | "dir": true, 9 | "modifiedIndex": 27, 10 | "createdIndex": 27 11 | }], 12 | "modifiedIndex": 27, 13 | "createdIndex": 27 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spec/fixtures/pools.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Pools", 5 | "dir": true, 6 | "nodes": [{ 7 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Pools/0", 8 | "dir": true, 9 | "nodes": [{ 10 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Pools/0/min_size", 11 | "value": "2", 12 | "modifiedIndex": 7098, 13 | "createdIndex": 7098 14 | }, { 15 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Pools/0/percent_used", 16 | "value": "0", 17 | "modifiedIndex": 7099, 18 | "createdIndex": 7099 19 | }, { 20 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Pools/0/pg_num", 21 | "value": "64", 22 | "modifiedIndex": 7100, 23 | "createdIndex": 7100 24 | }, { 25 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Pools/0/pool_id", 26 | "value": "0", 27 | "modifiedIndex": 7101, 28 | "createdIndex": 7101 29 | }, { 30 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Pools/0/pool_name", 31 | "value": "rbd", 32 | "modifiedIndex": 7102, 33 | "createdIndex": 7102 34 | }, { 35 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Pools/0/used", 36 | "value": "0", 37 | "modifiedIndex": 7103, 38 | "createdIndex": 7103 39 | }], 40 | "modifiedIndex": 3559, 41 | "createdIndex": 3559 42 | }], 43 | "modifiedIndex": 3559, 44 | "createdIndex": 3559 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spec/fixtures/quentin.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/_tendrl/users/quentin", 5 | "dir": true, 6 | "nodes": [{ 7 | "key": "/_tendrl/users/quentin/username", 8 | "value": "quentin", 9 | "modifiedIndex": 191, 10 | "createdIndex": 191 11 | }, { 12 | "key": "/_tendrl/users/quentin/access_token", 13 | "value": "d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8", 14 | "expiration": "2017-02-23T16:37:30.353662562Z", 15 | "ttl": 511606, 16 | "modifiedIndex": 197, 17 | "createdIndex": 197 18 | }, { 19 | "key": "/_tendrl/users/quentin/email", 20 | "value": "quentin@tendrl.org", 21 | "modifiedIndex": 190, 22 | "createdIndex": 190 23 | }, { 24 | "key": "/_tendrl/users/quentin/name", 25 | "value": "Quentin", 26 | "modifiedIndex": 189, 27 | "createdIndex": 189 28 | }, { 29 | "key": "/_tendrl/users/quentin/password_hash", 30 | "value": "$2a$10$2TEvrFp5cKo9dFvJSRyp6e7g5GzGzic7X4Xu1n.Cf2WCiiOuh3o.u", 31 | "modifiedIndex": 193, 32 | "createdIndex": 193 33 | }, { 34 | "key": "/_tendrl/users/quentin/password_salt", 35 | "value": "$2a$10$2TEvrFp5cKo9dFvJSRyp6e", 36 | "modifiedIndex": 192, 37 | "createdIndex": 192 38 | },{ 39 | "key": "/_tendrl/users/quentin/email_notifications", 40 | "value": "true", 41 | "modifiedIndex": 192, 42 | "createdIndex": 192 43 | },{ 44 | "key": "/_tendrl/users/quentin/role", 45 | "value": "normal", 46 | "modifiedIndex": 192, 47 | "createdIndex": 192 48 | }], 49 | "modifiedIndex": 188, 50 | "createdIndex": 188 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spec/fixtures/thardy.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/_tendrl/users/thardy", 5 | "dir": true, 6 | "nodes": [{ 7 | "key": "/_tendrl/users/thardy/username", 8 | "value": "thardy", 9 | "modifiedIndex": 191, 10 | "createdIndex": 191 11 | }, { 12 | "key": "/_tendrl/users/thardy/access_token", 13 | "value": "d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8", 14 | "expiration": "2017-02-23T16:37:30.353662562Z", 15 | "ttl": 511606, 16 | "modifiedIndex": 197, 17 | "createdIndex": 197 18 | }, { 19 | "key": "/_tendrl/users/thardy/email", 20 | "value": "thardy@tendrl.org", 21 | "modifiedIndex": 190, 22 | "createdIndex": 190 23 | }, { 24 | "key": "/_tendrl/users/thardy/name", 25 | "value": "Tom Hardy", 26 | "modifiedIndex": 189, 27 | "createdIndex": 189 28 | }, { 29 | "key": "/_tendrl/users/thardy/password_hash", 30 | "value": "$2a$10$2TEvrFp5cKo9dFvJSRyp6e7g5GzGzic7X4Xu1n.Cf2WCiiOuh3o.u", 31 | "modifiedIndex": 193, 32 | "createdIndex": 193 33 | }, { 34 | "key": "/_tendrl/users/thardy/password_salt", 35 | "value": "$2a$10$2TEvrFp5cKo9dFvJSRyp6e", 36 | "modifiedIndex": 192, 37 | "createdIndex": 192 38 | },{ 39 | "key": "/_tendrl/users/thardy/email_notifications", 40 | "value": "true", 41 | "modifiedIndex": 192, 42 | "createdIndex": 192 43 | },{ 44 | "key": "/_tendrl/users/thardy/role", 45 | "value": "limited", 46 | "modifiedIndex": 192, 47 | "createdIndex": 192 48 | }], 49 | "modifiedIndex": 188, 50 | "createdIndex": 188 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spec/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | --- !ruby/object:Etcd::Response 2 | action: get 3 | node: !ruby/object:Etcd::Node 4 | created_index: 65748 5 | modified_index: 65748 6 | ttl: 7 | key: /_tendrl/users 8 | value: 9 | expiration: 10 | dir: true 11 | children: 12 | - !ruby/object:Etcd::Node 13 | created_index: 65748 14 | modified_index: 65748 15 | ttl: 16 | key: /_tendrl/users/dwarner 17 | value: 18 | expiration: 19 | dir: true 20 | children: 21 | - !ruby/object:Etcd::Node 22 | created_index: 65748 23 | modified_index: 65748 24 | ttl: 25 | key: /_tendrl/users/dwarner/data 26 | value: '{"name":"David Warner","username":"dwarner","password_salt":"$2a$10$2TEvrFp5cKo9dFvJSRyp6e","password_hash":"$2a$10$ACiOy8CCN57RjCP5qyd4Oe3Jokl6Jaj25vrb617NZ6xU5CUSsJTOW","email":"dwarner@tendrl.org","role":"dwarner","email_notifications":false}' 27 | expiration: 28 | dir: 29 | - !ruby/object:Etcd::Node 30 | created_index: 480839 31 | modified_index: 480839 32 | ttl: 33 | key: /_tendrl/users/quentin 34 | value: 35 | expiration: 36 | dir: true 37 | children: 38 | - !ruby/object:Etcd::Node 39 | created_index: 531720 40 | modified_index: 531720 41 | ttl: 42 | key: /_tendrl/users/quentin/data 43 | value: '{"name":"Quentin","username":"quentin","password_salt":"$2a$10$2TEvrFp5cKo9dFvJSRyp6e","password_hash":"$2a$10$2TEvrFp5cKo9dFvJSRyp6e7g5GzGzic7X4Xu1n.Cf2WCiiOuh3o.u","email":"quentin@email.com","role":"normal","email_notifications":false}' 44 | expiration: 45 | dir: 46 | etcd_index: 1039888 47 | raft_index: 4011185 48 | raft_term: 10 49 | -------------------------------------------------------------------------------- /spec/fixtures/volumes.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get", 3 | "node": { 4 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes", 5 | "dir": true, 6 | "nodes": [{ 7 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d", 8 | "dir": true, 9 | "nodes": [{ 10 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/name", 11 | "value": "v1", 12 | "modifiedIndex": 648927, 13 | "createdIndex": 648927 14 | }, { 15 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/rebal_id", 16 | "value": "00000000-0000-0000-0000-000000000000", 17 | "modifiedIndex": 648932, 18 | "createdIndex": 648932 19 | }, { 20 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/replica_count", 21 | "value": "1", 22 | "modifiedIndex": 648938, 23 | "createdIndex": 648938 24 | }, { 25 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/stripe_count", 26 | "value": "1", 27 | "modifiedIndex": 648945, 28 | "createdIndex": 648945 29 | }, { 30 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/transport_type", 31 | "value": "tcp", 32 | "modifiedIndex": 648947, 33 | "createdIndex": 648947 34 | }, { 35 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/brick_count", 36 | "value": "1", 37 | "modifiedIndex": 648925, 38 | "createdIndex": 648925 39 | }, { 40 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/status", 41 | "value": "Started", 42 | "modifiedIndex": 648944, 43 | "createdIndex": 648944 44 | }, { 45 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/subvol_count", 46 | "value": "1", 47 | "modifiedIndex": 648946, 48 | "createdIndex": 648946 49 | }, { 50 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/vol_id", 51 | "value": "20f5ed23-9794-42f3-9f5a-3b4611db0b1d", 52 | "modifiedIndex": 648963, 53 | "createdIndex": 648963 54 | }, { 55 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/options", 56 | "dir": true, 57 | "nodes": [{ 58 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/options/nfs.disable", 59 | "value": "on", 60 | "modifiedIndex": 648956, 61 | "createdIndex": 648956 62 | }, { 63 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/options/transport.address-family", 64 | "value": "inet", 65 | "modifiedIndex": 648957, 66 | "createdIndex": 648957 67 | }, { 68 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/options/performance.readdir-ahead", 69 | "value": "on", 70 | "modifiedIndex": 648955, 71 | "createdIndex": 648955 72 | }], 73 | "modifiedIndex": 86, 74 | "createdIndex": 86 75 | }, { 76 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/snapd_inited", 77 | "value": "True", 78 | "modifiedIndex": 648942, 79 | "createdIndex": 648942 80 | }, { 81 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/quorum_status", 82 | "value": "not_applicable", 83 | "modifiedIndex": 648928, 84 | "createdIndex": 648928 85 | }, { 86 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/rebal_lookedup", 87 | "value": "0", 88 | "modifiedIndex": 648933, 89 | "createdIndex": 648933 90 | }, { 91 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/rebal_skipped", 92 | "value": "0", 93 | "modifiedIndex": 648934, 94 | "createdIndex": 648934 95 | }, { 96 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/rebal_status", 97 | "value": "not_started", 98 | "modifiedIndex": 648935, 99 | "createdIndex": 648935 100 | }, { 101 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/snap_count", 102 | "value": "0", 103 | "modifiedIndex": 648940, 104 | "createdIndex": 648940 105 | }, { 106 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/Bricks", 107 | "dir": true, 108 | "nodes": [{ 109 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/Bricks/dhcp43-181.lab.eng.blr.redhat.com:_root_vol1_b1", 110 | "dir": true, 111 | "nodes": [{ 112 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/Bricks/dhcp43-181.lab.eng.blr.redhat.com:_root_vol1_b1/status", 113 | "value": "Started", 114 | "modifiedIndex": 648953, 115 | "createdIndex": 648953 116 | }, { 117 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/Bricks/dhcp43-181.lab.eng.blr.redhat.com:_root_vol1_b1/vol_id", 118 | "value": "20f5ed23-9794-42f3-9f5a-3b4611db0b1d", 119 | "modifiedIndex": 648954, 120 | "createdIndex": 648954 121 | }, { 122 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/Bricks/dhcp43-181.lab.eng.blr.redhat.com:_root_vol1_b1/hostname", 123 | "value": "dhcp43-181.lab.eng.blr.redhat.com", 124 | "modifiedIndex": 648950, 125 | "createdIndex": 648950 126 | }, { 127 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/Bricks/dhcp43-181.lab.eng.blr.redhat.com:_root_vol1_b1/path", 128 | "value": "dhcp43-181.lab.eng.blr.redhat.com:/root/vol1/b1", 129 | "modifiedIndex": 648951, 130 | "createdIndex": 648951 131 | }, { 132 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/Bricks/dhcp43-181.lab.eng.blr.redhat.com:_root_vol1_b1/port", 133 | "value": "49152", 134 | "modifiedIndex": 648952, 135 | "createdIndex": 648952 136 | }], 137 | "modifiedIndex": 81, 138 | "createdIndex": 81 139 | }], 140 | "modifiedIndex": 81, 141 | "createdIndex": 81 142 | }, { 143 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/arbiter_count", 144 | "value": "0", 145 | "modifiedIndex": 648924, 146 | "createdIndex": 648924 147 | }, { 148 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/rebal_data", 149 | "value": "0Bytes", 150 | "modifiedIndex": 648929, 151 | "createdIndex": 648929 152 | }, { 153 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/rebal_failures", 154 | "value": "0", 155 | "modifiedIndex": 648930, 156 | "createdIndex": 648930 157 | }, { 158 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/rebal_files", 159 | "value": "0", 160 | "modifiedIndex": 648931, 161 | "createdIndex": 648931 162 | }, { 163 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/redundancy_count", 164 | "value": "0", 165 | "modifiedIndex": 648936, 166 | "createdIndex": 648936 167 | }, { 168 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/snapd_status", 169 | "value": "Offline", 170 | "modifiedIndex": 648943, 171 | "createdIndex": 648943 172 | }, { 173 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/vol_type", 174 | "value": "Distribute", 175 | "modifiedIndex": 648949, 176 | "createdIndex": 648949 177 | }, { 178 | "key": "/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/Volumes/20f5ed23-9794-42f3-9f5a-3b4611db0b1d/disperse_count", 179 | "value": "0", 180 | "modifiedIndex": 648926, 181 | "createdIndex": 648926 182 | }], 183 | "modifiedIndex": 58, 184 | "createdIndex": 58 185 | }], 186 | "modifiedIndex": 58, 187 | "createdIndex": 58 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /spec/forms/user_form_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Tendrl::UserForm do 4 | 5 | UserForm = Tendrl::UserForm 6 | 7 | before do 8 | stub_users 9 | end 10 | 11 | context 'create' do 12 | 13 | let(:user){ Tendrl::User.new } 14 | 15 | it 'with invalid attributes' do 16 | validator = UserForm.new(user, {}) 17 | expect(validator.valid?).to eq(false) 18 | expect(validator.errors[:username].size).to eq(2) 19 | expect(validator.errors[:email].size).to eq(1) 20 | expect(validator.errors[:name].size).to eq(2) 21 | expect(validator.errors[:password].size).to eq(1) 22 | expect(validator.errors[:role].size).to eq(1) 23 | expect(validator.errors[:email_notifications].size).to eq(1) 24 | end 25 | 26 | it 'with valid attributes' do 27 | validator = UserForm.new(user, { 28 | name: 'Tom Hardy', 29 | email: 'tom@tendrl.org', 30 | username: 'thardy', 31 | password: 'temp12345', 32 | role: Tendrl::User::ADMIN, 33 | email_notifications: true 34 | }) 35 | expect(validator.valid?).to eq(true) 36 | end 37 | 38 | it 'with existing username/email' do 39 | validator = UserForm.new(user, { 40 | name: 'David Warner', 41 | email: 'dwarner@tendrl.org', 42 | username: 'dwarner', 43 | password: 'temp12345', 44 | email_notifications: true, 45 | role: Tendrl::User::ADMIN 46 | }) 47 | expect(validator.valid?).to eq(false) 48 | expect(validator.errors[:username].size).to eq(1) 49 | expect(validator.errors[:email].size).to eq(1) 50 | end 51 | 52 | end 53 | 54 | context 'update' do 55 | let(:user){ 56 | stub_user('dwarner') 57 | Tendrl::User.find('dwarner') 58 | } 59 | 60 | it 'with valid attributes and no password' do 61 | validator = UserForm.new(user, name: 'T Hardy') 62 | expect(validator.valid?).to eq(true) 63 | end 64 | 65 | it 'with valid attributes and invalid password' do 66 | validator = UserForm.new(user, { 67 | password: 'temp', 68 | role: Tendrl::User::NORMAL 69 | }) 70 | expect(validator.valid?).to eq(false) 71 | expect(validator.errors[:password].size).to eq(1) 72 | end 73 | 74 | it 'with valid attributes and password' do 75 | validator = UserForm.new( 76 | user, 77 | name: 'Tom Hardy', 78 | email: 'tom@tendrl.org', 79 | password: 'temp12345', 80 | ) 81 | expect(validator.valid?).to eq(true) 82 | end 83 | 84 | it 'with existing username/email' do 85 | validator = UserForm.new( 86 | user, 87 | name: 'David Warner', 88 | username: 'dwarner', 89 | email: 'dwarner@tendrl.org', 90 | password: 'temp12345' 91 | ) 92 | expect(validator.valid?).to eq(true) 93 | end 94 | 95 | specify 'with different username or role not allowed' do 96 | validator = UserForm.new( 97 | user, 98 | name: 'David Warner', 99 | email: 'dwarner@tendrl.org', 100 | username: 'thardy', 101 | password: 'temp12345', 102 | role: Tendrl::User::LIMITED 103 | ) 104 | expect(validator.valid?).to eq(false) 105 | expect(validator.errors.messages.keys).to include(:username, :role) 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Tendrl::User do 4 | it 'create' do 5 | stub_email_notifications_index('dwarner', 'dwarner@tendrl.org') 6 | stub_user('dwarner') 7 | stub_user_create('dwarner') 8 | attributes = { 9 | email: 'dwarner@tendrl.org', 10 | name: 'David Warner', 11 | username: 'dwarner', 12 | password: 'temp1234' 13 | } 14 | stub_create_user_attributes(attributes) 15 | user = Tendrl::User.save(attributes) 16 | expect(user.username).to eq('dwarner') 17 | end 18 | 19 | context 'authentication' do 20 | 21 | before do 22 | stub_user('dwarner') 23 | stub_access_token('dwarner') 24 | end 25 | 26 | it 'with valid username and password' do 27 | user = Tendrl::User.authenticate('dwarner', 'temp1234') 28 | expect(user).to be_present 29 | expect(user.username).to eq('dwarner') 30 | end 31 | 32 | it 'with invalid username or password' do 33 | user = Tendrl::User.authenticate('dwarner', 'temp123') 34 | expect(user).to be_nil 35 | end 36 | 37 | it 'with valid access_token' do 38 | user = Tendrl::User.authenticate_access_token('d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8') 39 | expect(user).to be_present 40 | expect(user.username).to eq('dwarner') 41 | end 42 | 43 | it 'with invalid username or access_token' do 44 | user = Tendrl::User.authenticate('dwarner', 'blah') 45 | expect(user).to be_nil 46 | end 47 | 48 | end 49 | 50 | 51 | end 52 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = 'test' 2 | 3 | require 'simplecov' 4 | SimpleCov.start do 5 | add_filter "/spec/" 6 | end 7 | 8 | require './lib/tendrl' 9 | require './app/controllers/application_controller' 10 | require './app/controllers/authenticated_users_controller' 11 | require 'webmock/rspec' 12 | 13 | WebMock.disable_net_connect!(allow_localhost: false) 14 | 15 | RSpec.configure do |config| 16 | include Rack::Test::Methods 17 | 18 | config.filter_run_when_matching :focus 19 | 20 | def app 21 | described_class 22 | end 23 | 24 | config.before(:each) do 25 | $etcd_client = $etcd_client.dup 26 | allow(Tendrl.etcd).to receive(:get).and_call_original 27 | end 28 | end 29 | 30 | def stub_definitions 31 | defs = YAML.load_file 'spec/fixtures/definitions/tendrl_definitions_node_agent.yaml' 32 | allow(Tendrl).to receive(:load_node_definitions).and_return(defs) 33 | allow(Tendrl).to receive(:node_definitions).and_return(defs) 34 | allow(Tendrl).to receive(:current_definitions).and_return(defs) 35 | end 36 | 37 | def stub_cluster_definitions(type='gluster') 38 | defs = YAML.load_file "spec/fixtures/definitions/#{type}.yaml" 39 | allow(Tendrl).to receive(:load_definitions).and_return(defs) 40 | allow(Tendrl).to receive(:cluster_definitions).with('6b4b84e0-17b3-4543-af9f-e42000c52bfc').and_return(defs) 41 | allow(Tendrl).to receive(:current_definitions).and_return(defs) 42 | end 43 | 44 | def stub_nodes 45 | stub_request( 46 | :get, 47 | "http://127.0.0.1:4001/v2/keys/nodes?recursive=true" 48 | ). 49 | to_return( 50 | :status => 200, 51 | :body => File.read( 52 | 'spec/fixtures/nodes.json' 53 | ) 54 | ) 55 | end 56 | 57 | def stub_clusters(recursive=true) 58 | allow(Tendrl.etcd).to receive(:get).with('/clusters', recursive: true).and_return( 59 | YAML.load_file('spec/fixtures/clusters.yml') 60 | ) 61 | end 62 | 63 | def stub_cluster_context 64 | stub_request( 65 | :get, 66 | /http:\/\/127.0.0.1:4001\/v2\/keys\/clusters\/.*\/TendrlContext/i 67 | ). 68 | to_return( 69 | :status => 200, 70 | body: File.read('spec/fixtures/cluster_context.json') 71 | ) 72 | end 73 | 74 | def stub_cluster 75 | stub_request( 76 | :get, 77 | 'http://127.0.0.1:4001/v2/keys/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc?recursive=true' 78 | ). 79 | to_return( 80 | :status => 200, 81 | :body => File.read('spec/fixtures/cluster.json') 82 | ) 83 | end 84 | 85 | def stub_unmanaged_cluster 86 | stub_request( 87 | :get, 88 | 'http://127.0.0.1:4001/v2/keys/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc' 89 | ). 90 | to_return( 91 | :status => 200, 92 | body: File.read('spec/fixtures/unmanaged_clusters.json') 93 | ) 94 | end 95 | 96 | def stub_managed_cluster 97 | stub_request( 98 | :get, 99 | 'http://127.0.0.1:4001/v2/keys/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc' 100 | ). 101 | to_return( 102 | :status => 200, 103 | body: File.read('spec/fixtures/managed_clusters.json') 104 | ) 105 | end 106 | 107 | def stub_cluster_profiling 108 | stub_request( 109 | :put, 110 | "http://127.0.0.1:4001/v2/keys/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/volume_profiling_flag"). 111 | with(:body => "value=enable"). 112 | to_return(:status => 200, :body => "{\"action\":\"set\",\"node\":{\"key\":\"/clusters/6b4b84e0-17b3-4543-af9f-e42000c52bfc/volume_profiling_flag\",\"value\":\"enable\",\"modifiedIndex\":441,\"createdIndex\":441}}") 113 | end 114 | 115 | def stub_job_creation 116 | stub_request( 117 | :put, 118 | /http:\/\/127.0.0.1:4001\/v2\/keys\/queue\/.*/ 119 | ).to_return( 120 | status: 200, 121 | body: File.read('spec/fixtures/job_created.json') 122 | ) 123 | end 124 | 125 | def stub_pools 126 | stub_request( 127 | :get, 128 | /http:\/\/127.0.0.1:4001\/v2\/keys\/clusters\/.*\/Pools\?recursive=true/ 129 | ). 130 | to_return( 131 | :status => 200, 132 | :body => File.read( 133 | 'spec/fixtures/pools.json' 134 | ) 135 | ) 136 | end 137 | 138 | def stub_volumes 139 | stub_request( 140 | :get, 141 | /http:\/\/127.0.0.1:4001\/v2\/keys\/clusters\/.*\/Volumes\?recursive=true/ 142 | ). 143 | to_return( 144 | :status => 200, 145 | :body => File.read( 146 | 'spec/fixtures/volumes.json' 147 | ) 148 | ) 149 | end 150 | 151 | def stub_jobs 152 | stub_request( 153 | :get, 154 | "http://127.0.0.1:4001/v2/keys/queue?recursive=true" 155 | ). 156 | to_return( 157 | :status => 200, 158 | :body => File.read( 159 | 'spec/fixtures/jobs.json' 160 | ) 161 | ) 162 | end 163 | 164 | def stub_job 165 | allow(Tendrl.etcd).to receive(:get).with('/queue/3d38fb70-6865-4d77-82e0-22509359efef', recursive: true).and_return(YAML.load_file('spec/fixtures/job.yml')) 166 | end 167 | 168 | def stub_unknown_cluster 169 | stub_request( 170 | :get, 171 | /http:\/\/127.0.0.1:4001\/v2\/keys\/clusters\/unknown/ 172 | ). 173 | to_return( 174 | :status => 404, 175 | :body => { 176 | "errorCode" => 100, 177 | "message" => "Key not found", 178 | "cause" => "/clusters/unknown", 179 | "index"=> 51850 180 | }.to_json 181 | ) 182 | end 183 | 184 | def stub_missing_job 185 | stub_request( 186 | :get, 187 | /http:\/\/127.0.0.1:4001\/v2\/keys\/queue\/missing/ 188 | ). 189 | to_return( 190 | :status => 404, 191 | :body => { 192 | "errorCode" => 100, 193 | "message" => "Key not found", 194 | "cause" => "/queue/missing", 195 | "index"=> 51850 196 | }.to_json 197 | ) 198 | end 199 | 200 | def stub_node_ids 201 | stub_request( 202 | :get, 203 | /http:\/\/127.0.0.1:4001\/v2\/keys\/clusters\/.*\/nodes/i 204 | ). 205 | to_return( 206 | :status => 200, 207 | :body => File.read( 208 | 'spec/fixtures/node_ids.json' 209 | ) 210 | ) 211 | end 212 | 213 | def stub_create_user_attributes(attributes) 214 | attributes.merge!( 215 | password_hash: '$2a$10$d1L8axCcsr5XRMtzbNNuaOM5I6D9dKu0VJqifND/eHCnj8M1QkP1W', 216 | password_salt: '$2a$10$d1L8axCcsr5XRMtzbNNuaO' 217 | ) 218 | stub_request( 219 | :put, 220 | "http://127.0.0.1:4001/v2/keys/_tendrl/users/#{attributes[:username]}/data" 221 | ). 222 | to_return( 223 | :status => 200, 224 | :body => "{\"action\":\"set\",\"node\":{\"key\":\"/_tendrl/users/#{attributes[:username]}/data\",\"value\":#{CGI.unescape(attributes.to_json.to_json)},\"modifiedIndex\":184,\"createdIndex\":184}}" 225 | ) 226 | end 227 | 228 | def stub_update_user_attributes(username, attributes) 229 | attributes.each do |key, value| 230 | stub_request( 231 | :put, 232 | /http:\/\/127.0.0.1:4001\/v2\/keys\/_tendrl\/users\/#{username}\/.*/i 233 | ). 234 | to_return( 235 | :status => 200, 236 | :body => "{\"action\":\"set\",\"node\":{\"key\":\"/_tendrl/users/#{username}/#{key}\",\"value\":\"#{CGI.unescape(value.to_s)}\",\"modifiedIndex\":184,\"createdIndex\":184}}" 237 | ) 238 | end 239 | end 240 | 241 | def stub_users 242 | allow(Tendrl.etcd).to receive(:get).with('/_tendrl/users', recursive: true).and_return(YAML.load_file('spec/fixtures/users.yml')) 243 | end 244 | 245 | def stub_user(username) 246 | stub_request( 247 | :get, 248 | "http://127.0.0.1:4001/v2/keys/_tendrl/users/#{username}" 249 | ). 250 | to_return( 251 | :status => 200, 252 | :body => File.read("spec/fixtures/#{username}.json") 253 | ) 254 | end 255 | 256 | def stub_user_create(username) 257 | stub_request( 258 | :put, 259 | "http://127.0.0.1:4001/v2/keys/_tendrl/users/#{username}"). 260 | with( 261 | :body => "dir=true" 262 | ). 263 | to_return( 264 | :status => 201, 265 | :body => "{\"action\":\"set\",\"node\":{\"key\":\"/_tendrl/users/#{username}\",\"dir\":true,\"modifiedIndex\":437,\"createdIndex\":437}}", 266 | ) 267 | end 268 | 269 | def stub_create_existing_user(username) 270 | stub_request( 271 | :put, 272 | "http://127.0.0.1:4001/v2/keys/_tendrl/users/#{username}"). 273 | with( 274 | :body => "dir=true" 275 | ). 276 | to_return( 277 | :status => 200, 278 | :body => "{\"action\":\"set\",\"node\":{\"key\":\"/_tendrl/users/#{username}\",\"dir\":true,\"modifiedIndex\":454,\"createdIndex\":454}}", 279 | :headers => {} 280 | ) 281 | end 282 | 283 | def stub_failed_create_existing_user(username) 284 | stub_request( 285 | :put, 286 | "http://127.0.0.1:4001/v2/keys/_tendrl/users/#{username}"). 287 | with( 288 | :body => "dir=true" 289 | ). 290 | to_return( 291 | :status => 403, 292 | :body => "{\"errorCode\":102,\"message\":\"Not a file\",\"cause\":\"/_tendrl/users/meh\",\"index\":453}", 293 | :headers => {} 294 | ) 295 | end 296 | 297 | def stub_access_token(username) 298 | stub_request( 299 | :get, 300 | "http://127.0.0.1:4001/v2/keys/_tendrl/access_tokens/d03ebb195dbe6385a7caeda699f9930ff2e49f29c381ed82dc95aa642a7660b8"). 301 | to_return( 302 | :status => 200, 303 | :body => "{\"action\": \"get\",\"node\": {\"key\": \"/_tendrl/access_tokens/8d6ff8e9475dcf9815c3adce4aa3f6b999c31d29281d0cb90fe085d589b4a736\",\"value\": \"#{username}\",\"modifiedIndex\": 342,\"createdIndex\": 342}}" 304 | ) 305 | end 306 | 307 | def stub_delete_user(username) 308 | stub_request( 309 | :delete, 310 | "http://127.0.0.1:4001/v2/keys/_tendrl/users/#{username}?recursive=true"). 311 | to_return( 312 | :status => 200, 313 | :body => "{\"action\":\"delete\",\"node\":{\"key\":\"/_tendrl/users/#{username}\",\"dir\":true,\"modifiedIndex\":455,\"createdIndex\":454},\"prevNode\":{\"key\":\"/_tendrl/users/fff\",\"dir\":true,\"modifiedIndex\":454,\"createdIndex\":454}}" 314 | ) 315 | end 316 | 317 | def stub_email_notifications_index(username, email) 318 | stub_request( 319 | :put, 320 | "http://127.0.0.1:4001/v2/keys/_tendrl/indexes/notifications/email_notifications/#{username}"). 321 | with( 322 | :body => "value=#{CGI.escape(email)}" 323 | ). 324 | to_return( 325 | :status => 200, 326 | :body => "{\"action\":\"get\",\"node\":{\"key\":\"/_tendrl/indexes/notifications/email_notifications\",\"dir\":true,\"nodes\":[{\"key\":\"/_tendrl/indexes/notifications/email_notifications/#{username}\",\"value\":\"#{email}\",\"modifiedIndex\":456,\"createdIndex\":456}],\"modifiedIndex\":456,\"createdIndex\":456}}" 327 | ) 328 | end 329 | 330 | def stub_delete_email_notifications_index(username) 331 | stub_request( 332 | :delete, 333 | "http://127.0.0.1:4001/v2/keys/_tendrl/indexes/notifications/email_notifications/#{username}"). 334 | to_return( 335 | :status => 200, 336 | :body => "" 337 | ) 338 | end 339 | 340 | def stub_ip 341 | stub_request( 342 | :get, 343 | /http:\/\/127.0.0.1:4001\/v2\/keys\/indexes\/ip\/.*/i 344 | ). 345 | to_return( 346 | :status => 200, 347 | :body => File.read('spec/fixtures/ip.json'), 348 | ) 349 | end 350 | 351 | def stub_node 352 | stub_request( 353 | :get, 354 | /http:\/\/127.0.0.1:4001\/v2\/keys\/nodes\/.*/i 355 | ). 356 | to_return( 357 | :status => 200, 358 | :body => File.read('spec/fixtures/node.json') 359 | ) 360 | end 361 | -------------------------------------------------------------------------------- /spec/tendrl/flow_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Tendrl::Flow do 4 | context 'node' do 5 | before do 6 | Tendrl.node_definitions = YAML.load_file( 7 | 'spec/fixtures/definitions/master.yaml' 8 | ) 9 | end 10 | 11 | it 'ImportCluster' do 12 | flow = Tendrl::Flow.new( 13 | 'namespace.tendrl', 14 | 'ImportCluster' 15 | ) 16 | expect(flow.flow_name).to eq('ImportCluster') 17 | expect(flow.tags({ 'TendrlContext.integration_id' => '12345'})).to eq(['tendrl/integration/12345']) 18 | end 19 | end 20 | 21 | context 'cluster' do 22 | context 'gluster volume' do 23 | before do 24 | Tendrl.cluster_definitions = YAML.load_file( 25 | 'spec/fixtures/definitions/gluster.yaml' 26 | ) 27 | end 28 | 29 | specify 'StartProfiling' do 30 | flow = Tendrl::Flow.new( 31 | 'namespace.gluster', 32 | 'StartProfiling', 33 | 'Volume' 34 | ) 35 | expect(flow.flow_name).to eq('StartProfiling') 36 | expect(flow.tags({ 'TendrlContext.integration_id' => '12345'})).to eq(['provisioner/12345']) 37 | end 38 | 39 | specify 'StopProfiling' do 40 | flow = Tendrl::Flow.new( 41 | 'namespace.gluster', 42 | 'StopProfiling', 43 | 'Volume' 44 | ) 45 | expect(flow.flow_name).to eq('StopProfiling') 46 | expect(flow.tags({ 'TendrlContext.integration_id' => '12345'})).to eq(['provisioner/12345']) 47 | end 48 | end 49 | 50 | context 'ceph' do 51 | before do 52 | Tendrl.cluster_definitions = YAML.load_file( 53 | 'spec/fixtures/definitions/ceph.yaml' 54 | ) 55 | end 56 | 57 | it 'CreatePool' do 58 | flow = Tendrl::Flow.new( 59 | 'namespace.ceph', 60 | 'CreatePool' 61 | ) 62 | expect(flow.flow_name).to eq('CreatePool') 63 | expect(flow.tags('TendrlContext.integration_id' => '12345')).to eq(['tendrl/integration/12345']) 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/tendrl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Tendrl do 4 | 5 | end 6 | 7 | 8 | -------------------------------------------------------------------------------- /tendrl-api.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Tendrl Api Daemon 3 | Documentation=https://github.com/Tendrl/api/tree/master/docs 4 | After=network.target 5 | Requires=tendrl-node-agent.service 6 | 7 | [Service] 8 | Type=simple 9 | ExecStart=/usr/bin/puma -C /usr/share/tendrl-api/config/puma/production.rb 10 | ExecReload=/bin/kill -HUP $MAINPID 11 | KillMode=process 12 | User=tendrl-api 13 | Restart=on-failure 14 | PrivateTmp=true 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /tendrl-api.spec: -------------------------------------------------------------------------------- 1 | %global name tendrl-api 2 | %global app_group %{name} 3 | %global app_user %{name} 4 | %global install_dir %{_datadir}/%{name} 5 | %global config_dir %{_sysconfdir}/tendrl 6 | %global doc_dir %{_docdir}/%{name} 7 | %global log_dir %{_var}/log/tendrl/api 8 | %global tmp_dir %{_var}/tmp 9 | %global config_file %{config_dir}/etcd.yml 10 | 11 | Name: %{name} 12 | Version: 1.6.3 13 | Release: 7%{?dist} 14 | Summary: Collection of tendrl api extensions 15 | Group: Development/Languages 16 | License: LGPLv2+ 17 | URL: https://github.com/Tendrl/api 18 | Source0: %{name}-%{version}.tar.gz 19 | BuildArch: noarch 20 | 21 | BuildRequires: ruby 22 | BuildRequires: systemd-units 23 | BuildRequires: systemd 24 | 25 | Requires: ruby >= 2.0.0 26 | Requires: rubygem-activemodel >= 4.2.6 27 | Requires: rubygem-bcrypt >= 3.1.10 28 | Requires: rubygem-i18n >= 0.7.0 29 | Requires: rubygem-json 30 | Requires: rubygem-minitest >= 5.9.1 31 | Requires: rubygem-thread_safe >= 0.3.5 32 | Requires: rubygem-mixlib-log >= 1.7.1 33 | Requires: rubygem-puma >= 3.6.0 34 | Requires: rubygem-rake >= 0.9.6 35 | Requires: rubygem-rack >= 1.6.4 36 | Requires: rubygem-tilt >= 1.4.1 37 | Requires: rubygem-bundler >= 1.13.6 38 | Requires: rubygem-builder >= 3.1.0 39 | Requires: rubygem-tzinfo >= 1.2.2 40 | Requires: rubygem-etcd 41 | Requires: rubygem-rack-protection >= 1.5.3 42 | Requires: rubygem-activesupport >= 4.2.6 43 | Requires: rubygem-sinatra >= 1.4.5 44 | Requires: tendrl-node-agent 45 | 46 | %description 47 | Collection of tendrl api. 48 | 49 | %package httpd 50 | Summary: Tendrl api httpd 51 | Requires: %{name} = %{version}-%{release} 52 | BuildArch: noarch 53 | Requires: httpd 54 | 55 | %description httpd 56 | Tendrl API httpd configuration. 57 | 58 | %prep 59 | %setup 60 | 61 | %build 62 | 63 | %install 64 | install -m 0755 --directory $RPM_BUILD_ROOT%{log_dir} 65 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/app/controllers 66 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/app/forms 67 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/app/presenters 68 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/app/models 69 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/lib/tendrl/errors 70 | install -m 0755 --directory $RPM_BUILD_ROOT%{doc_dir}/config 71 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/public 72 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/config 73 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/config/puma 74 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/config/initializers 75 | install -m 0755 --directory $RPM_BUILD_ROOT%{install_dir}/.deploy 76 | install -Dm 0644 Rakefile *.ru Gemfile* $RPM_BUILD_ROOT%{install_dir} 77 | install -Dm 0644 app/controllers/*.rb $RPM_BUILD_ROOT%{install_dir}/app/controllers/ 78 | install -Dm 0644 app/forms/*.rb $RPM_BUILD_ROOT%{install_dir}/app/forms/ 79 | install -Dm 0644 app/presenters/*.rb $RPM_BUILD_ROOT%{install_dir}/app/presenters/ 80 | install -Dm 0644 app/models/*.rb $RPM_BUILD_ROOT%{install_dir}/app/models/ 81 | install -Dm 0644 lib/*.rb $RPM_BUILD_ROOT%{install_dir}/lib/ 82 | install -Dm 0644 lib/tendrl/*.rb $RPM_BUILD_ROOT%{install_dir}/lib/tendrl/ 83 | install -Dm 0644 lib/tendrl/errors/*.rb $RPM_BUILD_ROOT%{install_dir}/lib/tendrl/errors/ 84 | install -Dm 0644 tendrl-api.service $RPM_BUILD_ROOT%{_unitdir}/tendrl-api.service 85 | install -Dm 0640 config/etcd.sample.yml $RPM_BUILD_ROOT%{config_file} 86 | install -Dm 0644 README.adoc Rakefile $RPM_BUILD_ROOT%{doc_dir} 87 | install -Dm 0644 config/apache.vhost-ssl.sample $RPM_BUILD_ROOT%{_sysconfdir}/httpd/conf.d/00_tendrl-ssl.conf.sample 88 | install -Dm 0644 config/apache.vhost.sample $RPM_BUILD_ROOT%{_sysconfdir}/httpd/conf.d/tendrl.conf 89 | install -Dm 0644 config/puma/*.rb $RPM_BUILD_ROOT%{install_dir}/config/puma/ 90 | install -Dm 0644 config/initializers/*.rb $RPM_BUILD_ROOT%{install_dir}/config/initializers/ 91 | install -Dm 0644 firewalld/tendrl-api.xml $RPM_BUILD_ROOT%{_prefix}/lib/firewalld/services/tendrl-api.xml 92 | install -Dm 0644 config/tendrl-api_logrotate.conf $RPM_BUILD_ROOT%{_sysconfdir}/logrotate.d/tendrl-api_logrotate.conf 93 | 94 | # Symlink writable directories onto /var 95 | ln -s %{log_dir} $RPM_BUILD_ROOT%{install_dir}/log 96 | ln -s %{tmp_dir} $RPM_BUILD_ROOT%{install_dir}/tmp 97 | 98 | %pre 99 | getent group %{app_group} > /dev/null || \ 100 | groupadd -r %{app_group} 101 | getent passwd %{app_user} > /dev/null || \ 102 | useradd -r -d %{install_dir} -M -g %{app_group} \ 103 | -s /sbin/nologin %{app_user} 104 | 105 | %post httpd 106 | setsebool -P httpd_can_network_connect 1 107 | systemctl enable tendrl-api >/dev/null 2>&1 || : 108 | 109 | %post 110 | %systemd_post tendrl-api.service 111 | 112 | %preun 113 | %systemd_preun tendrl-api.service 114 | 115 | %postun 116 | %systemd_postun_with_restart tendrl-api.service 117 | 118 | %files 119 | %license LICENSE 120 | %dir %attr(0755, %{app_user}, %{app_group}) %{log_dir} 121 | %dir %{config_dir} 122 | %{install_dir}/ 123 | %{doc_dir}/ 124 | %{_unitdir}/tendrl-api.service 125 | %config(noreplace) %{_sysconfdir}/logrotate.d/tendrl-api_logrotate.conf 126 | %config(noreplace) %attr(0640, root, %{app_group}) %{config_file} 127 | %config(noreplace) %{_prefix}/lib/firewalld/services/tendrl-api.xml 128 | 129 | %files httpd 130 | %config(noreplace) %{_sysconfdir}/httpd/conf.d/00_tendrl-ssl.conf.sample 131 | %config(noreplace) %{_sysconfdir}/httpd/conf.d/tendrl.conf 132 | 133 | %changelog 134 | * Fri Jan 18 2019 Gowtham Shanmugasundaram - 1.6.3-8 135 | - Log rotation for tendrl log files 136 | 137 | * Fri Jul 27 2018 Shirshendu Mukherjee - 1.6.3-7 138 | - Bugfix for recursion when non-json 'data' attr is present 139 | 140 | * Wed Jul 04 2018 Shirshendu Mukherjee - 1.6.3-6 141 | - Restrict username length to 20 chars 142 | 143 | * Wed Jul 04 2018 Shirshendu Mukherjee - 1.6.3-5 144 | - Add job ID to job data for node-agent 145 | 146 | * Tue May 29 2018 Shirshendu Mukherjee - 1.6.3-4 147 | - Allow passing job flags to unmanage API 148 | 149 | * Fri May 04 2018 Shirshendu Mukherjee - 1.6.3-3 150 | - Bugfixes for user management 151 | 152 | * Wed Apr 25 2018 Shirshendu Mukherjee - 1.6.3-2 153 | - Bugfix for volume-bricks API 154 | 155 | * Wed Apr 18 2018 Shirshendu Mukherjee - 1.6.3-1 156 | - https://github.com/Tendrl/api/milestone/5 157 | - Object marshalling/unmarshalling to/from etcd 158 | - Allow short_name as an attribute for cluster 159 | 160 | * Thu Mar 22 2018 Shirshendu Mukherjee - 1.6.2-1 161 | - https://github.com/Tendrl/api/milestone/4 162 | - Bugfixes 163 | - API call for expand cluster 164 | 165 | * Wed Mar 07 2018 Rohan Kanade - 1.6.1-1 166 | - Bugfixes (https://github.com/Tendrl/api/milestone/3) 167 | 168 | * Sat Feb 17 2018 Rohan Kanade - 1.6.0-1 169 | - API to un-manage clusters managed by Tendrl 170 | 171 | * Fri Feb 02 2018 Rohan Kanade - 1.5.5-1 172 | - Adds brick_count per node to /clusters/:cluster_id/nodes 173 | - Adds api /clusters/:cluster_id/notifications 174 | - Adds api /cluster/:cluster_id/jobs 175 | - Adds volume alert count to /clusters/:cluster_id 176 | 177 | * Thu Nov 30 2017 Rohan Kanade - 1.5.4-4 178 | - Bugfixes 179 | 180 | * Mon Nov 27 2017 Rohan Kanade - 1.5.4-3 181 | - Supress service enable message during package update 182 | 183 | * Fri Nov 10 2017 Rohan Kanade - 1.5.4-2 184 | - Bugfixes tendrl-api v1.5.4 185 | 186 | * Thu Nov 02 2017 Rohan Kanade - 1.5.4-1 187 | - Release tendrl-api v1.5.4 188 | 189 | * Fri Oct 13 2017 Rohan Kanade - 1.5.3-2 190 | - Release tendrl-api v1.5.3 191 | 192 | * Thu Oct 12 2017 Rohan Kanade - 1.5.3-1 193 | - Release tendrl-api v1.5.3 194 | 195 | * Fri Sep 15 2017 Rohan Kanade - 1.5.2-1 196 | - Release tendrl-api v1.5.2 197 | 198 | * Fri Aug 25 2017 Rohan Kanade - 1.5.1-1 199 | - Release tendrl-api v1.5.1 200 | 201 | * Fri Aug 04 2017 Rohan Kanade - 1.5.0-1 202 | - Release tendrl-api v1.5.0 203 | 204 | * Mon Jun 19 2017 Rohan Kanade - 1.4.2-1 205 | - Release tendrl-api v1.4.2 206 | 207 | * Thu Jun 08 2017 Anup Nivargi - 1.4.1-1 208 | - Release tendrl-api v1.4.1 209 | 210 | * Fri Jun 02 2017 Rohan Kanade - 1.4.0-1 211 | - Release tendrl-api v1.4.0 212 | 213 | * Tue Apr 18 2017 Anup Nivargi - 1.2-3 214 | - Version bump to the 1.2.3 release. 215 | 216 | * Wed Apr 05 2017 Anup Nivargi - 1.2-2 217 | - Version bump to the 1.2.2 release. 218 | 219 | * Fri Jan 27 2017 Mrugesh Karnik - 1.2-1 220 | - Version bump to the 1.2 release. 221 | 222 | * Wed Nov 16 2016 Tim - 0.0.1-1 223 | - Initial package 224 | --------------------------------------------------------------------------------