├── .tool-versions ├── elixir_buildpack.config ├── assets ├── .babelrc ├── static │ ├── favicon.ico │ ├── images │ │ ├── phoenix.png │ │ ├── preview.gif │ │ └── volumes.png │ └── robots.txt ├── css │ ├── app.css │ ├── dashboard.css │ ├── phoenix.css │ └── pure-min.css ├── js │ ├── app.js │ └── socket.js ├── package.json └── webpack.config.js ├── priv ├── repo │ ├── migrations │ │ ├── .formatter.exs │ │ ├── 20190823142303_create_peers.exs │ │ ├── 20190823142454_create_options.exs │ │ ├── 20190823142439_create_subvols.exs │ │ ├── 20190823142420_create_volumes.exs │ │ └── 20190823142514_create_bricks.exs │ └── seeds.exs └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── test ├── test_helper.exs ├── live_view_demo_web │ ├── views │ │ ├── page_view_test.exs │ │ ├── layout_view_test.exs │ │ └── error_view_test.exs │ └── controllers │ │ └── page_controller_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── data_case.ex └── live_view_demo │ └── gluster_test.exs ├── lib ├── gluster_dashboard_web │ ├── views │ │ ├── layout_view.ex │ │ ├── dashboard_view.ex │ │ ├── bricks_view.ex │ │ ├── peers_view.ex │ │ ├── volumes_view.ex │ │ ├── voldetails_view.ex │ │ ├── error_view.ex │ │ ├── error_helpers.ex │ │ └── helper_view.ex │ ├── controllers │ │ ├── dashboard_controller.ex │ │ └── webhook_controller.ex │ ├── live │ │ ├── peers_live.ex │ │ ├── bricks_live.ex │ │ ├── volumes_live.ex │ │ └── voldetails_live.ex │ ├── templates │ │ ├── peers │ │ │ └── index.html.leex │ │ ├── layout │ │ │ ├── dashboard.html.eex │ │ │ └── app.html.eex │ │ ├── volumes │ │ │ └── index.html.leex │ │ ├── bricks │ │ │ └── index.html.leex │ │ ├── dashboard │ │ │ └── index.html.eex │ │ └── voldetails │ │ │ └── index.html.leex │ ├── gettext.ex │ ├── router.ex │ ├── channels │ │ └── user_socket.ex │ └── endpoint.ex ├── gluster_dashboard │ ├── repo.ex │ ├── release.ex │ ├── gluster │ │ ├── option.ex │ │ ├── peer.ex │ │ ├── subvol.ex │ │ ├── brick.ex │ │ └── volume.ex │ ├── application.ex │ └── gluster.ex ├── gluster_dashboard.ex └── gluster_dashboard_web.ex ├── gluster-webhook-exporter ├── dub.selections.json ├── .gitignore ├── dub.sdl └── source │ └── app.d ├── .formatter.exs ├── config ├── releases.exs ├── test.exs ├── config.exs ├── prod.secret.exs ├── prod.exs └── dev.exs ├── README.md ├── .gitignore ├── extra └── phx.gen.sh ├── mix.exs └── mix.lock /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.9.1 2 | erlang 22.0.7 3 | nodejs 12.6.0 4 | -------------------------------------------------------------------------------- /elixir_buildpack.config: -------------------------------------------------------------------------------- 1 | elixir_version=1.9 2 | erlang_version=21.3 -------------------------------------------------------------------------------- /assets/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(GlusterDashboard.Repo, :manual) 3 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aravindavk/gluster-dashboard/HEAD/assets/static/favicon.ico -------------------------------------------------------------------------------- /assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aravindavk/gluster-dashboard/HEAD/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /assets/static/images/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aravindavk/gluster-dashboard/HEAD/assets/static/images/preview.gif -------------------------------------------------------------------------------- /assets/static/images/volumes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aravindavk/gluster-dashboard/HEAD/assets/static/images/volumes.png -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.LayoutView do 2 | use GlusterDashboardWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/views/dashboard_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.DashboardView do 2 | use GlusterDashboardWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /test/live_view_demo_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.PageViewTest do 2 | use GlusterDashboardWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/live_view_demo_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.LayoutViewTest do 2 | use GlusterDashboardWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /gluster-webhook-exporter/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "arsd-official": "4.0.2", 5 | "glustercli": "0.1.2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/gluster_dashboard/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Repo do 2 | use Ecto.Repo, 3 | otp_app: :gluster_dashboard, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :phoenix], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/views/bricks_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.BricksView do 2 | use GlusterDashboardWeb, :view 3 | import GlusterDashboardWeb.HelperView 4 | 5 | end 6 | 7 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/views/peers_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.PeersView do 2 | use GlusterDashboardWeb, :view 3 | import GlusterDashboardWeb.HelperView 4 | 5 | end 6 | 7 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/views/volumes_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.VolumesView do 2 | use GlusterDashboardWeb, :view 3 | import GlusterDashboardWeb.HelperView 4 | 5 | end 6 | 7 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/views/voldetails_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.VoldetailsView do 2 | use GlusterDashboardWeb, :view 3 | import GlusterDashboardWeb.HelperView 4 | 5 | end 6 | 7 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ 2 | 3 | @import "./phoenix.css"; 4 | @import "../../deps/phoenix_live_view/assets/css/live_view.css"; 5 | @import "./pure-min.css"; 6 | @import "./dashboard.css"; 7 | -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /config/releases.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :gluster_dashboard, GlusterDashboardWeb.Endpoint, 4 | server: true, 5 | http: [port: {:system, "PORT"}], # Needed for Phoenix 1.2 and 1.4. Doesn't hurt for 1.3. 6 | url: [host: System.get_env("APP_NAME") <> ".gigalixirapp.com", port: 443] 7 | -------------------------------------------------------------------------------- /test/live_view_demo_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.PageControllerTest do 2 | use GlusterDashboardWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, "/") 6 | assert html_response(conn, 200) =~ "My Phrenzy Entry" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/gluster_dashboard.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard do 2 | @moduledoc """ 3 | GlusterDashboard keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /gluster-webhook-exporter/.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | gluster-webhook-exporter 5 | gluster-webhook-exporter.so 6 | gluster-webhook-exporter.dylib 7 | gluster-webhook-exporter.dll 8 | gluster-webhook-exporter.a 9 | gluster-webhook-exporter.lib 10 | gluster-webhook-exporter-test-* 11 | *.exe 12 | *.o 13 | *.obj 14 | *.lst 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20190823142303_create_peers.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Repo.Migrations.CreatePeers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:peers, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :address, :string 8 | add :state, :string 9 | 10 | timestamps() 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # GlusterDashboard.Repo.insert!(%GlusterDashboard.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /gluster-webhook-exporter/dub.sdl: -------------------------------------------------------------------------------- 1 | name "gluster-webhook-exporter" 2 | description "Export Gluster Volume and Peer info to Webhook" 3 | authors "Aravinda VK " 4 | copyright "Copyright © 2019, Aravinda VK " 5 | license "Apache-2.0" 6 | dependency "glustercli" version="~>0.1.2" 7 | dependency "arsd-official:dom" version="~>4.0.2" 8 | 9 | dflags "-static" "-link-defaultlib-shared=false" platform="linux-ldc" 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20190823142454_create_options.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Repo.Migrations.CreateOptions do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:options) do 6 | add :name, :string 7 | add :value, :string 8 | add :volumes_id, references(:volumes, on_delete: :nothing, type: :string) 9 | 10 | timestamps() 11 | end 12 | 13 | create index(:options, [:volumes_id]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/gluster_dashboard/release.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Release do 2 | @app :gluster_dashboard 3 | 4 | def migrate do 5 | for repo <- repos() do 6 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) 7 | end 8 | end 9 | 10 | def rollback(repo, version) do 11 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) 12 | end 13 | 14 | defp repos do 15 | Application.load(@app) 16 | Application.fetch_env!(@app, :ecto_repos) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/live_view_demo_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.ErrorViewTest do 2 | use GlusterDashboardWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(GlusterDashboardWeb.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(GlusterDashboardWeb.ErrorView, "500.html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/gluster_dashboard/gluster/option.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Gluster.Option do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "options" do 6 | field :name, :string 7 | field :value, :string 8 | 9 | belongs_to :volumes, GlusterDashboard.Gluster.Volume, [type: :string, foreign_key: :volumes_id] 10 | 11 | timestamps() 12 | end 13 | 14 | @doc false 15 | def changeset(opton, attrs) do 16 | opton 17 | |> cast(attrs, [:name, :value, :volumes_id]) 18 | |> validate_required([:name, :value, :volumes_id]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // We need to import the CSS so that webpack will load it. 2 | // The MiniCssExtractPlugin is used to separate it out into 3 | // its own CSS file. 4 | import css from "../css/app.css" 5 | 6 | // webpack automatically bundles all modules in your 7 | // entry points. Those entry points can be configured 8 | // in "webpack.config.js". 9 | // 10 | // Import dependencies 11 | // 12 | import "phoenix_html" 13 | 14 | import {Socket} from "phoenix" 15 | import LiveSocket from "phoenix_live_view" 16 | 17 | let liveSocket = new LiveSocket("/live", Socket) 18 | liveSocket.connect() 19 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :gluster_dashboard, GlusterDashboard.Repo, 5 | username: "postgres", 6 | password: "postgres", 7 | database: "gluster_dashboard_test", 8 | hostname: "localhost", 9 | pool: Ecto.Adapters.SQL.Sandbox 10 | 11 | # We don't run a server during test. If one is required, 12 | # you can enable the server option below. 13 | config :gluster_dashboard, GlusterDashboardWeb.Endpoint, 14 | http: [port: 4002], 15 | server: false 16 | 17 | # Print only warnings and errors during test 18 | config :logger, level: :warn 19 | -------------------------------------------------------------------------------- /lib/gluster_dashboard/gluster/peer.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Gluster.Peer do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @primary_key {:id, :string, autogenerate: false} 6 | schema "peers" do 7 | field :address, :string 8 | field :state, :string 9 | 10 | has_many :bricks, GlusterDashboard.Gluster.Brick, foreign_key: :peers_id 11 | 12 | timestamps() 13 | end 14 | 15 | @doc false 16 | def changeset(peer, attrs) do 17 | peer 18 | |> cast(attrs, [:id, :address, :state]) 19 | |> validate_required([:id, :address, :state]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.ErrorView do 2 | use GlusterDashboardWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gluster-dashboard 2 | 3 | Gluster Dashboard application using Elixir Phoenix liveview feature. 4 | 5 | This project is initially created for [Phoenix Phrenzy](https://phoenixphrenzy.com) contest, showing off what [Phoenix](https://phoenixframework.org/) and [LiveView](https://github.com/phoenixframework/phoenix_live_view) can do. 6 | 7 | ![Gluster dashboard preview](assets/static/images/preview.gif "Gluster Dashboard") 8 | 9 | ## Deployment 10 | 11 | Coming soon. 12 | 13 | ## Talks and Blog posts 14 | 15 | * [BLOG] [Elixir Phoenix Liveview and Gluster dashboard](https://aravindavk.in/blog/elixir-phoenix-liveview-gluster-dashboard/) 16 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/controllers/dashboard_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.DashboardController do 2 | use GlusterDashboardWeb, :controller 3 | 4 | plug :put_layout, "dashboard.html" 5 | 6 | def volumes(conn, _params) do 7 | render(conn, "index.html", name: "volumes") 8 | end 9 | 10 | def volumes_detail(conn, %{"volname" => volname}) do 11 | render(conn, "index.html", name: "volumes-detail", volname: volname) 12 | end 13 | 14 | def peers(conn, _params) do 15 | render(conn, "index.html", name: "peers") 16 | end 17 | 18 | def bricks(conn, _params) do 19 | render(conn, "index.html", name: "bricks") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/live/peers_live.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.PeersLive do 2 | use Phoenix.LiveView 3 | alias GlusterDashboardWeb.PeersView 4 | alias GlusterDashboard.Gluster 5 | 6 | @topic inspect(__MODULE__) 7 | 8 | def render(assigns) do 9 | PeersView.render("index.html", assigns) 10 | end 11 | 12 | def mount(_session, socket) do 13 | Gluster.subscribe() 14 | {:ok, put_peers_details(socket)} 15 | end 16 | 17 | def handle_info({Gluster, :cluster_changed}, socket) do 18 | {:noreply, put_peers_details(socket)} 19 | end 20 | 21 | defp put_peers_details(socket) do 22 | assign(socket, peers: Gluster.list_peers(), counts: Gluster.get_counts()) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /priv/repo/migrations/20190823142439_create_subvols.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Repo.Migrations.CreateSubvols do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:subvols, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :health, :string 8 | add :replica_count, :integer 9 | add :arbiter_count, :integer 10 | add :disperse_count, :integer 11 | add :disperse_redundancy_count, :integer 12 | add :type, :string 13 | add :num_bricks, :integer 14 | add :volumes_id, references(:volumes, on_delete: :nothing, type: :string) 15 | 16 | timestamps() 17 | end 18 | 19 | create index(:subvols, [:volumes_id]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/live/bricks_live.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.BricksLive do 2 | use Phoenix.LiveView 3 | alias GlusterDashboardWeb.BricksView 4 | alias GlusterDashboard.Gluster 5 | 6 | @topic inspect(__MODULE__) 7 | 8 | def render(assigns) do 9 | BricksView.render("index.html", assigns) 10 | end 11 | 12 | def mount(_session, socket) do 13 | Gluster.subscribe() 14 | {:ok, put_bricks_details(socket)} 15 | end 16 | 17 | def handle_info({Gluster, :cluster_changed}, socket) do 18 | {:noreply, put_bricks_details(socket)} 19 | end 20 | 21 | defp put_bricks_details(socket) do 22 | assign(socket, bricks: Gluster.list_bricks(), counts: Gluster.get_counts()) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/templates/peers/index.html.leex: -------------------------------------------------------------------------------- 1 |
2 | <%= top_summary(@counts, Enum.at(@peers, 0)) %> 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <%= for peer <- @peers do %> 13 | 14 | 15 | 20 | 21 | <% end %> 22 | 23 |
PeerState
<%= peer.address %> 16 | 17 | <%= peer.state %> 18 | 19 |
24 |
25 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/live/volumes_live.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.VolumesLive do 2 | use Phoenix.LiveView 3 | alias GlusterDashboardWeb.VolumesView 4 | alias GlusterDashboard.Gluster 5 | 6 | @topic inspect(__MODULE__) 7 | 8 | def render(assigns) do 9 | VolumesView.render("index.html", assigns) 10 | end 11 | 12 | def mount(_session, socket) do 13 | Gluster.subscribe() 14 | {:ok, put_volumes_details(socket)} 15 | end 16 | 17 | def handle_info({Gluster, :cluster_changed}, socket) do 18 | {:noreply, put_volumes_details(socket)} 19 | end 20 | 21 | defp put_volumes_details(socket) do 22 | vols = Gluster.list_volumes() 23 | counts = Gluster.get_counts() 24 | assign(socket, volumes: vols, counts: counts) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "webpack --mode production", 6 | "watch": "webpack --mode development --watch" 7 | }, 8 | "dependencies": { 9 | "phoenix": "file:../deps/phoenix", 10 | "phoenix_html": "file:../deps/phoenix_html", 11 | "phoenix_live_view": "file:../deps/phoenix_live_view" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.0.0", 15 | "@babel/preset-env": "^7.0.0", 16 | "babel-loader": "^8.0.0", 17 | "copy-webpack-plugin": "^4.5.0", 18 | "css-loader": "^2.1.1", 19 | "mini-css-extract-plugin": "^0.4.0", 20 | "optimize-css-assets-webpack-plugin": "^5.0.3", 21 | "uglifyjs-webpack-plugin": "^1.2.4", 22 | "webpack": "4.4.0", 23 | "webpack-cli": "^3.3.6" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import GlusterDashboardWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :gluster_dashboard 24 | end 25 | -------------------------------------------------------------------------------- /priv/repo/migrations/20190823142420_create_volumes.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Repo.Migrations.CreateVolumes do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:volumes, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :name, :string 8 | add :type, :string 9 | add :state, :string 10 | add :health, :string 11 | add :num_subvols, :integer 12 | add :num_bricks, :integer 13 | add :replica_count, :integer 14 | add :arbiter_count, :integer 15 | add :disperse_count, :integer 16 | add :disperse_redundancy_count, :integer 17 | add :transport, :string 18 | add :size_total, :bigint 19 | add :size_used, :bigint 20 | add :inodes_total, :bigint 21 | add :inodes_used, :bigint 22 | 23 | timestamps() 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/live/voldetails_live.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.VoldetailsLive do 2 | use Phoenix.LiveView 3 | alias GlusterDashboardWeb.VoldetailsView 4 | alias GlusterDashboard.Gluster 5 | 6 | @topic inspect(__MODULE__) 7 | 8 | def render(assigns) do 9 | VoldetailsView.render("index.html", assigns) 10 | end 11 | 12 | def mount(%{volname: volname}, socket) do 13 | Gluster.subscribe() 14 | socket = assign(socket, volname: volname) 15 | {:ok, put_volumes_details(socket)} 16 | end 17 | 18 | def handle_info({Gluster, :cluster_changed}, socket) do 19 | {:noreply, put_volumes_details(socket)} 20 | end 21 | 22 | defp put_volumes_details(socket) do 23 | details = Gluster.get_volume_by_name(socket.assigns.volname) 24 | counts = Gluster.get_counts() 25 | assign(socket, volume: details, counts: counts) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/controllers/webhook_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.WebhookController do 2 | use GlusterDashboardWeb, :controller 3 | alias GlusterDashboard.Gluster 4 | 5 | def create_or_update(conn, %{"peers" => peers, "volumes" => volumes}) do 6 | # TODO: Handle failure 7 | Gluster.delete_all() 8 | 9 | peers |> Enum.map(&Gluster.create_peer/1) 10 | 11 | for volume <- volumes do 12 | Gluster.create_volume(volume) 13 | 14 | for opt <- volume["options"] do 15 | Gluster.create_option(opt) 16 | end 17 | 18 | for subvol <- volume["subvols"] do 19 | Gluster.create_subvol(subvol) 20 | 21 | for brick <- subvol["bricks"] do 22 | IO.inspect Gluster.create_brick(brick) 23 | end 24 | end 25 | end 26 | 27 | Gluster.broadcast_change(:cluster_changed) 28 | json(conn, %{ok: true}) 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/templates/layout/dashboard.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Gluster Dashboard 8 | "/> 9 | 10 | 11 |
12 |
13 |
14 | 15 | 16 | <%= render @view_module, @view_template, assigns %> 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /priv/repo/migrations/20190823142514_create_bricks.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Repo.Migrations.CreateBricks do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:bricks) do 6 | add :path, :string 7 | add :state, :string 8 | add :fs, :string 9 | add :device, :string 10 | add :mount_options, :string 11 | add :block_size, :integer 12 | add :size_total, :bigint 13 | add :size_used, :bigint 14 | add :inodes_total, :bigint 15 | add :inodes_used, :bigint 16 | add :pid, :integer 17 | add :port, :integer 18 | add :volumes_id, references(:volumes, on_delete: :nothing, type: :string) 19 | add :subvols_id, references(:subvols, on_delete: :nothing, type: :string) 20 | add :peers_id, references(:peers, on_delete: :nothing, type: :string) 21 | 22 | timestamps() 23 | end 24 | 25 | create index(:bricks, [:volumes_id]) 26 | create index(:bricks, [:subvols_id]) 27 | create index(:bricks, [:peers_id]) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | gluster_dashboard-*.tar 24 | 25 | # If NPM crashes, it generates a log, let's ignore it too. 26 | npm-debug.log 27 | 28 | # The directory NPM downloads your dependencies sources to. 29 | /assets/node_modules/ 30 | 31 | # Since we are building assets from assets/, 32 | # we ignore priv/static. You may want to comment 33 | # this depending on your deployment strategy. 34 | /priv/static/ 35 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.Router do 2 | use GlusterDashboardWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug Phoenix.LiveView.Flash 8 | plug :fetch_flash 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | scope "/", GlusterDashboardWeb do 18 | pipe_through :browser 19 | 20 | get "/", DashboardController, :volumes 21 | get "/dashboard", DashboardController, :volumes 22 | get "/dashboard/volumes", DashboardController, :volumes 23 | get "/dashboard/volumes/:volname", DashboardController, :volumes_detail 24 | get "/dashboard/peers", DashboardController, :peers 25 | get "/dashboard/bricks", DashboardController, :bricks 26 | end 27 | 28 | # Other scopes may use custom stacks. 29 | scope "/api", GlusterDashboardWeb do 30 | pipe_through :api 31 | 32 | post "/webhook", WebhookController, :create_or_update 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/gluster_dashboard/gluster/subvol.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Gluster.Subvol do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @primary_key {:id, :string, autogenerate: false} 6 | schema "subvols" do 7 | field :arbiter_count, :integer 8 | field :disperse_count, :integer 9 | field :disperse_redundancy_count, :integer 10 | field :health, :string 11 | field :num_bricks, :integer 12 | field :replica_count, :integer 13 | field :type, :string 14 | 15 | belongs_to :volumes, GlusterDashboard.Gluster.Volume, [type: :string, foreign_key: :volumes_id] 16 | has_many :bricks, GlusterDashboard.Gluster.Brick, foreign_key: :subvols_id 17 | 18 | timestamps() 19 | end 20 | 21 | @doc false 22 | def changeset(subvol, attrs) do 23 | subvol 24 | |> cast(attrs, [:id, :health, :replica_count, :arbiter_count, :disperse_count, :disperse_redundancy_count, :type, :num_bricks, :volumes_id]) 25 | |> validate_required([:id, :health, :replica_count, :arbiter_count, :disperse_count, :disperse_redundancy_count, :type, :num_bricks, :volumes_id]) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/gluster_dashboard/application.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | # List all child processes to be supervised 10 | children = [ 11 | # Start the Ecto repository 12 | GlusterDashboard.Repo, 13 | # Start the endpoint when the application starts 14 | GlusterDashboardWeb.Endpoint 15 | # Starts a worker by calling: GlusterDashboard.Worker.start_link(arg) 16 | # {GlusterDashboard.Worker, arg}, 17 | ] 18 | 19 | # See https://hexdocs.pm/elixir/Supervisor.html 20 | # for other strategies and supported options 21 | opts = [strategy: :one_for_one, name: GlusterDashboard.Supervisor] 22 | Supervisor.start_link(children, opts) 23 | end 24 | 25 | # Tell Phoenix to update the endpoint configuration 26 | # whenever the application is updated. 27 | def config_change(changed, _new, removed) do 28 | GlusterDashboardWeb.Endpoint.config_change(changed, removed) 29 | :ok 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint GlusterDashboardWeb.Endpoint 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(GlusterDashboard.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(GlusterDashboard.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | use Mix.Config 9 | 10 | config :gluster_dashboard, 11 | ecto_repos: [GlusterDashboard.Repo] 12 | 13 | # Configures the endpoint 14 | config :gluster_dashboard, GlusterDashboardWeb.Endpoint, 15 | url: [host: "localhost"], 16 | secret_key_base: "dummy_val_for_dev_env", 17 | render_errors: [view: GlusterDashboardWeb.ErrorView, accepts: ~w(html json)], 18 | pubsub: [name: GlusterDashboard.PubSub, adapter: Phoenix.PubSub.PG2], 19 | live_view: [ 20 | signing_salt: "another_dummy_val_for_dev_env" 21 | ] 22 | 23 | # Configures Elixir's Logger 24 | config :logger, :console, 25 | format: "$time $metadata[$level] $message\n", 26 | metadata: [:request_id] 27 | 28 | # Use Jason for JSON parsing in Phoenix 29 | config :phoenix, :json_library, Jason 30 | 31 | # Import environment specific config. This must remain at the bottom 32 | # of this file so it overrides the configuration defined above. 33 | import_config "#{Mix.env()}.exs" 34 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", GlusterDashboardWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | def connect(_params, socket, _connect_info) do 19 | {:ok, socket} 20 | end 21 | 22 | # Socket id's are topics that allow you to identify all sockets for a given user: 23 | # 24 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 25 | # 26 | # Would allow you to broadcast a "disconnect" event and terminate 27 | # all active sockets and channels for a given user: 28 | # 29 | # GlusterDashboardWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 30 | # 31 | # Returning `nil` makes this socket anonymous. 32 | def id(_socket), do: nil 33 | end 34 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | alias GlusterDashboardWeb.Router.Helpers, as: Routes 23 | 24 | # The default endpoint for testing 25 | @endpoint GlusterDashboardWeb.Endpoint 26 | end 27 | end 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(GlusterDashboard.Repo) 31 | 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(GlusterDashboard.Repo, {:shared, self()}) 34 | end 35 | 36 | {:ok, conn: Phoenix.ConnTest.build_conn()} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 6 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 7 | 8 | module.exports = (env, options) => ({ 9 | optimization: { 10 | minimizer: [ 11 | new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }), 12 | new OptimizeCSSAssetsPlugin({}) 13 | ] 14 | }, 15 | entry: { 16 | './js/app.js': glob.sync('./vendor/**/*.js').concat(['./js/app.js']) 17 | }, 18 | output: { 19 | filename: 'app.js', 20 | path: path.resolve(__dirname, '../priv/static/js') 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | exclude: /node_modules/, 27 | use: { 28 | loader: 'babel-loader' 29 | } 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | new MiniCssExtractPlugin({ filename: '../css/app.css' }), 39 | new CopyWebpackPlugin([{ from: 'static/', to: '../' }]) 40 | ] 41 | }); 42 | -------------------------------------------------------------------------------- /lib/gluster_dashboard/gluster/brick.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Gluster.Brick do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "bricks" do 6 | field :block_size, :integer 7 | field :device, :string 8 | field :fs, :string 9 | field :inodes_total, :integer 10 | field :inodes_used, :integer 11 | field :mount_options, :string 12 | field :path, :string 13 | field :pid, :integer 14 | field :port, :integer 15 | field :size_total, :integer 16 | field :size_used, :integer 17 | field :state, :string 18 | 19 | belongs_to :subvols, GlusterDashboard.Gluster.Subvol, [type: :string, foreign_key: :subvols_id] 20 | belongs_to :peers, GlusterDashboard.Gluster.Peer, [type: :string, foreign_key: :peers_id] 21 | belongs_to :volumes, GlusterDashboard.Gluster.Volume, [type: :string, foreign_key: :volumes_id] 22 | 23 | timestamps() 24 | end 25 | 26 | @doc false 27 | def changeset(brick, attrs) do 28 | brick 29 | |> cast(attrs, [:path, :state, :fs, :device, :mount_options, :block_size, :size_total, :size_used, :inodes_total, :inodes_used, :pid, :port, :volumes_id, :subvols_id, :peers_id]) 30 | |> validate_required([:path, :state, :block_size, :size_total, :size_used, :inodes_total, :inodes_used, :volumes_id, :subvols_id, :peers_id]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GlusterDashboard · Phoenix Framework 8 | "/> 9 | 10 | 11 |
12 |
13 | 18 | 21 |
22 |
23 |
24 | 25 | 26 | <%= render @view_module, @view_template, assigns %> 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | # In this file, we load production configuration and secrets 2 | # from environment variables. You can also hardcode secrets, 3 | # although such is generally not recommended and you have to 4 | # remember to add this file to your .gitignore. 5 | use Mix.Config 6 | 7 | database_url = 8 | System.get_env("DATABASE_URL") || 9 | raise """ 10 | environment variable DATABASE_URL is missing. 11 | For example: ecto://USER:PASS@HOST/DATABASE 12 | """ 13 | 14 | config :gluster_dashboard, GlusterDashboard.Repo, 15 | # ssl: true, 16 | url: database_url, 17 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") 18 | 19 | secret_key_base = 20 | System.get_env("SECRET_KEY_BASE") || 21 | raise """ 22 | environment variable SECRET_KEY_BASE is missing. 23 | You can generate one by calling: mix phx.gen.secret 24 | """ 25 | 26 | config :gluster_dashboard, GlusterDashboardWeb.Endpoint, 27 | http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")], 28 | secret_key_base: secret_key_base 29 | 30 | # ## Using releases (Elixir v1.9+) 31 | # 32 | # If you are doing OTP releases, you need to instruct Phoenix 33 | # to start each relevant endpoint: 34 | # 35 | # config :gluster_dashboard, GlusterDashboardWeb.Endpoint, server: true 36 | # 37 | # Then you can assemble a release by calling `mix release`. 38 | # See `mix help release` for more information. 39 | -------------------------------------------------------------------------------- /lib/gluster_dashboard/gluster/volume.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Gluster.Volume do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @primary_key {:id, :string, autogenerate: false} 6 | schema "volumes" do 7 | field :arbiter_count, :integer 8 | field :disperse_count, :integer 9 | field :disperse_redundancy_count, :integer 10 | field :health, :string 11 | field :inodes_total, :integer 12 | field :inodes_used, :integer 13 | field :name, :string 14 | field :num_bricks, :integer 15 | field :num_subvols, :integer 16 | field :replica_count, :integer 17 | field :size_total, :integer 18 | field :size_used, :integer 19 | field :state, :string 20 | field :transport, :string 21 | field :type, :string 22 | 23 | has_many :subvols, GlusterDashboard.Gluster.Subvol, foreign_key: :volumes_id 24 | has_many :bricks, GlusterDashboard.Gluster.Brick, foreign_key: :volumes_id 25 | has_many :options, GlusterDashboard.Gluster.Option, foreign_key: :volumes_id 26 | 27 | timestamps() 28 | end 29 | 30 | @doc false 31 | def changeset(volume, attrs) do 32 | volume 33 | |> cast(attrs, [:id, :name, :type, :state, :health, :num_subvols, :num_bricks, :replica_count, :arbiter_count, :disperse_count, :disperse_redundancy_count, :transport, :size_total, :size_used, :inodes_total, :inodes_used]) 34 | |> validate_required([:id, :name, :type, :state, :health, :num_subvols, :num_bricks, :replica_count, :arbiter_count, :disperse_count, :disperse_redundancy_count, :transport, :size_total, :size_used, :inodes_total, :inodes_used]) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :gluster_dashboard 3 | 4 | socket "/socket", GlusterDashboardWeb.UserSocket, 5 | websocket: true, 6 | longpoll: false 7 | 8 | socket "/live", Phoenix.LiveView.Socket 9 | 10 | # Serve at "/" the static files from "priv/static" directory. 11 | # 12 | # You should set gzip to true if you are running phx.digest 13 | # when deploying your static files in production. 14 | plug Plug.Static, 15 | at: "/", 16 | from: :gluster_dashboard, 17 | gzip: false, 18 | only: ~w(css fonts images js favicon.ico robots.txt) 19 | 20 | # Code reloading can be explicitly enabled under the 21 | # :code_reloader configuration of your endpoint. 22 | if code_reloading? do 23 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 24 | plug Phoenix.LiveReloader 25 | plug Phoenix.CodeReloader 26 | end 27 | 28 | plug Plug.RequestId 29 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 30 | 31 | plug Plug.Parsers, 32 | parsers: [:urlencoded, :multipart, :json], 33 | pass: ["*/*"], 34 | json_decoder: Phoenix.json_library() 35 | 36 | plug Plug.MethodOverride 37 | plug Plug.Head 38 | 39 | # The session will be stored in the cookie and signed, 40 | # this means its contents can be read but not tampered with. 41 | # Set :encryption_salt if you would also like to encrypt it. 42 | plug Plug.Session, 43 | store: :cookie, 44 | key: "_gluster_dashboard_key", 45 | signing_salt: "pIQrFsE9" 46 | 47 | plug GlusterDashboardWeb.Router 48 | end 49 | -------------------------------------------------------------------------------- /extra/phx.gen.sh: -------------------------------------------------------------------------------- 1 | # Peers Schema 2 | mix phx.gen.context Gluster Peer peers peerid:string address:string state:string 3 | 4 | # Volumes Schema 5 | mix phx.gen.context Gluster Volume volumes \ 6 | volid:string \ 7 | name:string \ 8 | type:string \ 9 | state:string \ 10 | health:string \ 11 | num_subvols:integer \ 12 | num_bricks:integer \ 13 | replica_count:integer \ 14 | arbiter_count:integer \ 15 | disperse_count:integer \ 16 | disperse_redundancy_count:integer \ 17 | transport:string \ 18 | size_total:integer \ 19 | size_used:integer \ 20 | inodes_total:integer \ 21 | inodes_used:integer 22 | 23 | # Subvols Schema 24 | mix phx.gen.context Gluster Subvol subvols \ 25 | volume_id:references:volumes \ 26 | name:string \ 27 | health:string \ 28 | replica_count:integer \ 29 | arbiter_count:integer \ 30 | disperse_count:integer \ 31 | disperse_redundancy_count:integer \ 32 | type:string \ 33 | num_bricks:integer 34 | 35 | # Options Schema 36 | mix phx.gen.context Gluster Opton options \ 37 | volume_id:references:volumes \ 38 | name:string \ 39 | value:string 40 | 41 | # Bricks Schema 42 | mix phx.gen.context Gluster Brick bricks \ 43 | volume_id:references:volumes \ 44 | subvol_id:references:subvols \ 45 | peer_id:references:peers \ 46 | path:string \ 47 | state:string \ 48 | fs:string \ 49 | device:string \ 50 | mount_options:string \ 51 | block_size:integer \ 52 | size_total:integer \ 53 | size_used:integer \ 54 | inodes_total:integer \ 55 | inodes_used:integer \ 56 | pid:integer \ 57 | port:integer 58 | 59 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias GlusterDashboard.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import GlusterDashboard.DataCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(GlusterDashboard.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(GlusterDashboard.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | A helper that transforms changeset errors into a map of messages. 40 | 41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 42 | assert "password is too short" in errors_on(changeset).password 43 | assert %{password: ["password is too short"]} = errors_on(changeset) 44 | 45 | """ 46 | def errors_on(changeset) do 47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 48 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 49 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 50 | end) 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), class: "help-block") 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # When using gettext, we typically pass the strings we want 22 | # to translate as a static argument: 23 | # 24 | # # Translate "is invalid" in the "errors" domain 25 | # dgettext("errors", "is invalid") 26 | # 27 | # # Translate the number of files with plural rules 28 | # dngettext("errors", "1 file", "%{count} files", count) 29 | # 30 | # Because the error messages we show in our forms and APIs 31 | # are defined inside Ecto, we need to translate them dynamically. 32 | # This requires us to call the Gettext module passing our gettext 33 | # backend as first argument. 34 | # 35 | # Note we use the "errors" domain, which means translations 36 | # should be written to the errors.po file. The :count option is 37 | # set by Ecto and indicates we should also apply plural rules. 38 | if count = opts[:count] do 39 | Gettext.dngettext(GlusterDashboardWeb.Gettext, "errors", msg, msg, count, opts) 40 | else 41 | Gettext.dgettext(GlusterDashboardWeb.Gettext, "errors", msg, opts) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/templates/volumes/index.html.leex: -------------------------------------------------------------------------------- 1 |
2 | <%= top_summary(@counts, Enum.at(@volumes, 0)) %> 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <%= for volume <- @volumes do %> 16 | onclick="window.location='/dashboard/volumes/<%= volume.name %>';"> 17 | 20 | 25 | 29 | 32 | 35 | 36 | <% end %> 37 | 38 |
VolumeStateTypeUtilizationInodes Utilization
<%= volume.name %>
18 | <%= volume.id %> 19 |
21 | 22 | <%= volume_status(volume) %> 23 | 24 | 26 | <%= volume.type %>
27 | <%= get_volume_type_details(volume) %><%= get_subvol_details(volume) %> 28 |
30 | <%= utilization(volume.size_used, volume.size_total, false) %> 31 | 33 | <%= utilization(volume.inodes_used, volume.inodes_total, true) %> 34 |
39 |
40 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/templates/bricks/index.html.leex: -------------------------------------------------------------------------------- 1 |
2 | <%= top_summary(@counts, Enum.at(@bricks, 0)) %> 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <%= for brick <- @bricks do %> 19 | 20 | 23 | 26 | 30 | 31 | 34 | 37 | 38 | 39 | 40 | <% end %> 41 | 42 |
Volume nameBrickStateFSUtilizationInodes UtilizationPIDPort
<%= brick.volumes.name %>
21 | <%= brick.volumes_id %> 22 |
<%= brick.peers.address %>:<%= brick.path %>
24 | Peer ID: <%= brick.peers_id %> 25 |
27 | <%= brick.state %> 28 | 29 | <%= brick.fs %> 32 | <%= utilization(brick.size_used, brick.size_total, false) %> 33 | 35 | <%= utilization(brick.inodes_used, brick.inodes_total, true) %> 36 | <%= brick.pid %><%= brick.port %>
43 |
44 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/templates/dashboard/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

glusterdash

3 |
4 |
5 | 20 |
21 | <%= if @name == "bricks" do %> 22 |

Bricks

23 | <%= live_render(@conn, GlusterDashboardWeb.BricksLive) %> 24 | <% end %> 25 | 26 | <%= if @name == "peers" do %> 27 |

Peers

28 | <%= live_render(@conn, GlusterDashboardWeb.PeersLive) %> 29 | <% end %> 30 | 31 | <%= if @name == "volumes" do %> 32 |

Volumes

33 | <%= live_render(@conn, GlusterDashboardWeb.VolumesLive) %> 34 | <% end %> 35 | 36 | <%= if @name == "volumes-detail" do %> 37 |

Volumes/<%= @volname %>

38 | <%= live_render(@conn, GlusterDashboardWeb.VoldetailsLive, session: %{volname: @volname}) %> 39 | <% end %> 40 | 41 |
42 |
43 | 44 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :gluster_dashboard, 7 | version: "0.1.0", 8 | elixir: "~> 1.9.0", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {GlusterDashboard.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.4.9"}, 37 | {:phoenix_pubsub, "~> 1.1"}, 38 | {:phoenix_ecto, "~> 4.0"}, 39 | {:phoenix_live_view, "~> 0.3.0"}, 40 | {:ecto_sql, "~> 3.1"}, 41 | {:postgrex, ">= 0.0.0"}, 42 | {:phoenix_html, "~> 2.11"}, 43 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 44 | {:gettext, "~> 0.11"}, 45 | {:jason, "~> 1.0"}, 46 | {:plug_cowboy, "~> 2.0"}, 47 | {:calendar, "~> 0.17.6"}, 48 | {:timex, "~> 3.6.1"} 49 | ] 50 | end 51 | 52 | # Aliases are shortcuts or tasks specific to the current project. 53 | # For example, to create, migrate and run the seeds file at once: 54 | # 55 | # $ mix ecto.setup 56 | # 57 | # See the documentation for `Mix` for more info on aliases. 58 | defp aliases do 59 | [ 60 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 61 | "ecto.reset": ["ecto.drop", "ecto.setup"], 62 | test: ["ecto.create --quiet", "ecto.migrate", "test"] 63 | ] 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /gluster-webhook-exporter/source/app.d: -------------------------------------------------------------------------------- 1 | import std.net.curl: HTTP, post; 2 | import std.json; 3 | import std.algorithm: map; 4 | import std.array: array; 5 | import std.getopt; 6 | import core.thread; 7 | import std.experimental.logger; 8 | 9 | import glustercli; 10 | 11 | int main(string[] args) 12 | { 13 | string url; 14 | int interval = 30; 15 | bool verbose; 16 | 17 | try 18 | { 19 | auto helpInformation = getopt( 20 | args, 21 | std.getopt.config.required, 22 | "u|url", "Webhook URL", &url, 23 | "t|interval", "Webhook push interval", &interval, 24 | "v|verbose", "Verbose output", &verbose); 25 | 26 | if (helpInformation.helpWanted) 27 | { 28 | defaultGetoptPrinter("Usage: gluster-webhook-exporter [OPTIONS]", 29 | helpInformation.options); 30 | return 0; 31 | } 32 | } 33 | catch (GetOptException ex) 34 | { 35 | import std.stdio: stderr; 36 | 37 | stderr.writeln("Invalid inputs: ", ex.msg); 38 | return 1; 39 | } 40 | 41 | // Minimum interval is 5 seconds 42 | if (interval <= 5) 43 | interval = 5; 44 | 45 | // Default log level as info. Set to trace if --verbose is set 46 | sharedLog.logLevel = LogLevel.info; 47 | if (verbose) 48 | { 49 | sharedLog.logLevel = LogLevel.trace; 50 | } 51 | 52 | // Summary 53 | infof("exporter started. [{url=%s}, {interval_seconds=%d}, {verbose=%s}]", 54 | url, interval, verbose); 55 | 56 | while (true) 57 | { 58 | auto jsonVal = JSONValue( 59 | [ 60 | "peers": peerStatus().map!(peer => peer.toJson).array, 61 | "volumes": volumeStatus().map!(vol => vol.toJson).array 62 | ]); 63 | auto details = jsonVal.toJSON(false, JSONOptions.doNotEscapeSlashes); 64 | trace(details); 65 | auto http = HTTP(); 66 | http.addRequestHeader("Content-Type", "application/json"); 67 | auto content = post(url, details, http); 68 | info("pushed peer and volume details to the webhook"); 69 | Thread.sleep(dur!("seconds")(interval)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :gluster_dashboard, GlusterDashboardWeb.Endpoint, 13 | url: [host: "example.com", port: 80], 14 | cache_static_manifest: "priv/static/cache_manifest.json" 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | # ## SSL Support 20 | # 21 | # To get SSL working, you will need to add the `https` key 22 | # to the previous section and set your `:url` port to 443: 23 | # 24 | # config :gluster_dashboard, GlusterDashboardWeb.Endpoint, 25 | # ... 26 | # url: [host: "example.com", port: 443], 27 | # https: [ 28 | # :inet6, 29 | # port: 443, 30 | # cipher_suite: :strong, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 33 | # ] 34 | # 35 | # The `cipher_suite` is set to `:strong` to support only the 36 | # latest and more secure SSL ciphers. This means old browsers 37 | # and clients may not be supported. You can set it to 38 | # `:compatible` for wider support. 39 | # 40 | # `:keyfile` and `:certfile` expect an absolute path to the key 41 | # and cert in disk or a relative path inside priv, for example 42 | # "priv/ssl/server.key". For all supported SSL configuration 43 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 44 | # 45 | # We also recommend setting `force_ssl` in your endpoint, ensuring 46 | # no data is ever sent via http, always redirecting to https: 47 | # 48 | # config :gluster_dashboard, GlusterDashboardWeb.Endpoint, 49 | # force_ssl: [hsts: true] 50 | # 51 | # Check `Plug.SSL` for all available options in `force_ssl`. 52 | 53 | # Finally import the config/prod.secret.exs which loads secrets 54 | # and configuration from environment variables. 55 | import_config "prod.secret.exs" 56 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use GlusterDashboardWeb, :controller 9 | use GlusterDashboardWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: GlusterDashboardWeb 23 | 24 | import Plug.Conn 25 | import GlusterDashboardWeb.Gettext 26 | alias GlusterDashboardWeb.Router.Helpers, as: Routes 27 | import Phoenix.LiveView.Controller, only: [live_render: 3] 28 | end 29 | end 30 | 31 | def view do 32 | quote do 33 | use Phoenix.View, 34 | root: "lib/gluster_dashboard_web/templates", 35 | namespace: GlusterDashboardWeb 36 | 37 | # Import convenience functions from controllers 38 | import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] 39 | 40 | # Use all HTML functionality (forms, tags, etc) 41 | use Phoenix.HTML 42 | 43 | import GlusterDashboardWeb.ErrorHelpers 44 | import GlusterDashboardWeb.Gettext 45 | import Phoenix.LiveView, only: [live_render: 2, live_render: 3, live_link: 1, live_link: 2] 46 | alias GlusterDashboardWeb.Router.Helpers, as: Routes 47 | end 48 | end 49 | 50 | def router do 51 | quote do 52 | use Phoenix.Router 53 | import Plug.Conn 54 | import Phoenix.Controller 55 | import Phoenix.LiveView.Router 56 | end 57 | end 58 | 59 | def channel do 60 | quote do 61 | use Phoenix.Channel 62 | import GlusterDashboardWeb.Gettext 63 | end 64 | end 65 | 66 | @doc """ 67 | When used, dispatch to the appropriate controller/view/etc. 68 | """ 69 | defmacro __using__(which) when is_atom(which) do 70 | apply(__MODULE__, which, []) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket, 5 | // and connect at the socket path in "lib/web/endpoint.ex". 6 | // 7 | // Pass the token on params as below. Or remove it 8 | // from the params if you are not using authentication. 9 | import {Socket} from "phoenix" 10 | 11 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 12 | 13 | // When you connect, you'll often need to authenticate the client. 14 | // For example, imagine you have an authentication plug, `MyAuth`, 15 | // which authenticates the session and assigns a `:current_user`. 16 | // If the current user exists you can assign the user's token in 17 | // the connection for use in the layout. 18 | // 19 | // In your "lib/web/router.ex": 20 | // 21 | // pipeline :browser do 22 | // ... 23 | // plug MyAuth 24 | // plug :put_user_token 25 | // end 26 | // 27 | // defp put_user_token(conn, _) do 28 | // if current_user = conn.assigns[:current_user] do 29 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 30 | // assign(conn, :user_token, token) 31 | // else 32 | // conn 33 | // end 34 | // end 35 | // 36 | // Now you need to pass this token to JavaScript. You can do so 37 | // inside a script tag in "lib/web/templates/layout/app.html.eex": 38 | // 39 | // 40 | // 41 | // You will need to verify the user token in the "connect/3" function 42 | // in "lib/web/channels/user_socket.ex": 43 | // 44 | // def connect(%{"token" => token}, socket, _connect_info) do 45 | // # max_age: 1209600 is equivalent to two weeks in seconds 46 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 47 | // {:ok, user_id} -> 48 | // {:ok, assign(socket, :user, user_id)} 49 | // {:error, reason} -> 50 | // :error 51 | // end 52 | // end 53 | // 54 | // Finally, connect to the socket: 55 | socket.connect() 56 | 57 | // Now that you are connected, you can join channels with a topic: 58 | let channel = socket.channel("topic:subtopic", {}) 59 | channel.join() 60 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 61 | .receive("error", resp => { console.log("Unable to join", resp) }) 62 | 63 | export default socket 64 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :gluster_dashboard, GlusterDashboard.Repo, 5 | username: "postgres", 6 | password: "postgres", 7 | database: "gluster_dashboard_dev", 8 | hostname: "localhost", 9 | show_sensitive_data_on_connection_error: true, 10 | pool_size: 10 11 | 12 | # For development, we disable any cache and enable 13 | # debugging and code reloading. 14 | # 15 | # The watchers configuration can be used to run external 16 | # watchers to your application. For example, we use it 17 | # with webpack to recompile .js and .css sources. 18 | config :gluster_dashboard, GlusterDashboardWeb.Endpoint, 19 | http: [port: 4000], 20 | debug_errors: true, 21 | code_reloader: true, 22 | check_origin: false, 23 | watchers: [ 24 | node: [ 25 | "node_modules/webpack/bin/webpack.js", 26 | "--mode", 27 | "development", 28 | "--watch-stdin", 29 | cd: Path.expand("../assets", __DIR__) 30 | ] 31 | ] 32 | 33 | # ## SSL Support 34 | # 35 | # In order to use HTTPS in development, a self-signed 36 | # certificate can be generated by running the following 37 | # Mix task: 38 | # 39 | # mix phx.gen.cert 40 | # 41 | # Note that this task requires Erlang/OTP 20 or later. 42 | # Run `mix help phx.gen.cert` for more information. 43 | # 44 | # The `http:` config above can be replaced with: 45 | # 46 | # https: [ 47 | # port: 4001, 48 | # cipher_suite: :strong, 49 | # keyfile: "priv/cert/selfsigned_key.pem", 50 | # certfile: "priv/cert/selfsigned.pem" 51 | # ], 52 | # 53 | # If desired, both `http:` and `https:` keys can be 54 | # configured to run both http and https servers on 55 | # different ports. 56 | 57 | # Watch static and templates for browser reloading. 58 | config :gluster_dashboard, GlusterDashboardWeb.Endpoint, 59 | live_reload: [ 60 | patterns: [ 61 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 62 | ~r"priv/gettext/.*(po)$", 63 | ~r"lib/gluster_dashboard_web/(live|views)/.*(ex)$", 64 | ~r"lib/gluster_dashboard_web/templates/.*(eex)$", 65 | ~r{lib/live_view/live/.*(ex)$} 66 | ] 67 | ] 68 | 69 | # Do not include metadata nor timestamps in development logs 70 | config :logger, :console, format: "[$level] $message\n" 71 | 72 | # Set a higher stacktrace during development. Avoid configuring such 73 | # in production as building large stacktraces may be expensive. 74 | config :phoenix, :stacktrace_depth, 20 75 | 76 | # Initialize plugs at runtime for faster development compilation 77 | config :phoenix, :plug_init_mode, :runtime 78 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/templates/voldetails/index.html.leex: -------------------------------------------------------------------------------- 1 |
2 | <%= top_summary(@counts, @volume) %> 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | > 16 | 19 | 24 | 28 | 31 | 34 | 35 | 36 |
VolumeStateTypeUtilizationInodes Utilization
<%= @volume.name %>
17 | <%= @volume.id %> 18 |
20 | 21 | <%= volume_status(@volume) %> 22 | 23 | 25 | <%= @volume.type %>
26 | <%= get_volume_type_details(@volume) %><%= get_subvol_details(@volume) %> 27 |
29 | <%= utilization(@volume.size_used, @volume.size_total, false) %> 30 | 32 | <%= utilization(@volume.inodes_used, @volume.inodes_total, true) %> 33 |
37 | 38 |

Bricks

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | <%= for brick <- @volume.bricks do %> 53 | 54 | 57 | 61 | 62 | 65 | 68 | 69 | 70 | 71 | <% end %> 72 | 73 |
BrickStateFSUtilizationInodes UtilizationPIDPort
<%= brick.peers.address %>:<%= brick.path %>
55 | Peer ID: <%= brick.peers_id %> 56 |
58 | <%= brick.state %> 59 | 60 | <%= brick.fs %> 63 | <%= utilization(brick.size_used, brick.size_total, false) %> 64 | 66 | <%= utilization(brick.inodes_used, brick.inodes_total, true) %> 67 | <%= brick.pid %><%= brick.port %>
74 | 75 |

Options

76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | <%= for opt <- @volume.options do %> 85 | 86 | 87 | 88 | 89 | <% end %> 90 | 91 |
NameValue
<%= opt.name %><%= opt.value %>
92 |
93 | -------------------------------------------------------------------------------- /lib/gluster_dashboard_web/views/helper_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboardWeb.HelperView do 2 | import Phoenix.HTML 3 | 4 | def volume_status(volume) do 5 | status_suffix = 6 | if volume.state == "Started" do 7 | ", " <> volume.health 8 | else 9 | ", -" 10 | end 11 | volume.state <> status_suffix 12 | end 13 | 14 | def peer_state_htmlclass(peer) do 15 | case peer.state do 16 | "Online" -> "peer-state-online" 17 | _ -> "peer-state-offline" 18 | end 19 | end 20 | 21 | def brick_state_htmlclass(brick) do 22 | case brick.state do 23 | "up" -> "brick-state-online" 24 | _ -> "brick-state-offline" 25 | end 26 | end 27 | 28 | def volume_state_htmlclass(volume) do 29 | if volume.state == "Started" do 30 | case volume.health do 31 | "partial" -> "volume-state-partial" 32 | "degraded" -> "volume-state-degraded" 33 | "down" -> "volume-state-down" 34 | _ -> "volume-state-up" 35 | end 36 | else 37 | "volume-state-stopped" 38 | end 39 | end 40 | 41 | defp float_to_string(val) do 42 | val 43 | |> Float.round(1) 44 | |> Float.to_string 45 | end 46 | 47 | defp human_readable(input_size, raw) do 48 | kib = 1024 49 | mib = 1024 * kib 50 | gib = 1024 * mib 51 | tib = 1024 * gib 52 | pib = 1024 * tib 53 | eib = 1024 * pib 54 | zib = 1024 * eib 55 | yib = 1024 * zib 56 | 57 | if raw or input_size < 1024 do 58 | input_size 59 | else 60 | cond do 61 | (input_size < mib) -> float_to_string(input_size/kib) <> " KiB" 62 | (input_size < gib) -> float_to_string(input_size/mib) <> " MiB" 63 | (input_size < tib) -> float_to_string(input_size/gib) <> " GiB" 64 | (input_size < pib) -> float_to_string(input_size/tib) <> " TiB" 65 | (input_size < eib) -> float_to_string(input_size/pib) <> " PiB" 66 | (input_size < zib) -> float_to_string(input_size/eib) <> " EiB" 67 | (input_size < yib) -> float_to_string(input_size/zib) <> " ZiB" 68 | true -> float_to_string(input_size/yib) <> " YiB" 69 | end 70 | end 71 | end 72 | 73 | def utilization(used_value, total_value, raw) do 74 | if total_value <= 0 do 75 | "-" 76 | else 77 | valueP = trunc(used_value * 100 / total_value) 78 | classname = "utilization-above-" <> Integer.to_string(valueP - rem(valueP, 10)) 79 | v1 = human_readable(used_value, raw) 80 | v2 = human_readable(total_value, raw) 81 | ~E""" 82 |
83 | <%= valueP %> % 84 |
85 |
86 |
87 |
88 | 89 | <%= v1 %> / <%= v2 %> 90 | 91 |
92 | """ 93 | end 94 | end 95 | 96 | def get_volume_type_details(volume) do 97 | cond do 98 | # (volume.arbiter_count > 0) -> "Replica: " <> Integer.to_string(volume.replica_count) <> " Arbiter: " <> Integer.to_string(volume.arbiter_count) 99 | (volume.replica_count > 1) -> "Replica: " <> Integer.to_string(volume.replica_count) 100 | # (volume.disperse_count > 1) -> "Disperse: " <> Integer.to_string(volume.disperse_count) 101 | true -> "" 102 | end 103 | end 104 | 105 | def get_subvol_details(volume) do 106 | " Subvolumes: " <> (volume.num_subvols |> Integer.to_string) <> " Bricks: " <> (volume.num_bricks |> Integer.to_string) 107 | end 108 | 109 | def top_summary(counts, obj) do 110 | import Timex.Timezone 111 | 112 | ~E""" 113 | <%= if obj != nil do %> 114 |
115 | Last updated <%= convert(obj.updated_at, local()) |> Calendar.Strftime.strftime!("%b %d, %Y %l:%M %P") %> 116 |
117 | <% end %> 118 |
119 |
120 | Number of Volumes <%= counts.volumes %> 121 |
122 |
123 | Number of Peers <%= counts.peers %> 124 |
125 |
126 | Number of Bricks <%= counts.bricks %> 127 |
128 |
129 | """ 130 | end 131 | 132 | end 133 | 134 | -------------------------------------------------------------------------------- /assets/css/dashboard.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background-color: #eee; 3 | margin:0; 4 | padding: 0; 5 | } 6 | a, a:active, a:focus{ 7 | outline:none !important; 8 | } 9 | .dashboard-header{ 10 | position: fixed; 11 | background-color: #fff; 12 | width: 100%; 13 | color: #fff; 14 | box-shadow: 0 0 11px rgba(0,0,0,0.14); 15 | z-index: 2000; 16 | top:0; 17 | } 18 | .menu-title{ 19 | float: left; 20 | margin-left: 240px; 21 | color: #222; 22 | } 23 | .sidebar{ 24 | padding-top: 70px; 25 | position: fixed; 26 | float: left; 27 | width: 170px; 28 | background-color: #2D4256; 29 | height: 100%; 30 | color: #fff; 31 | box-shadow: 5px 5px 15px #444; 32 | } 33 | .dashboard-header h2{ 34 | margin: 0; 35 | padding: 10px; 36 | width: 170px; 37 | height: 50px; 38 | background-color: #263544; 39 | } 40 | .logo-first{ 41 | color: #fff; 42 | } 43 | .logo-last{ 44 | color: gold; 45 | } 46 | .sidebar ul{ 47 | list-style-type: none; 48 | margin: 0; 49 | padding-left: 0px; 50 | } 51 | .sidebar li{ 52 | /* padding: 5px 10px; */ 53 | } 54 | .sidebar li a{ 55 | text-decoration: none; 56 | color: #eee; 57 | } 58 | 59 | .sidebar li{ 60 | border-left: 7px solid #2D4256; 61 | } 62 | 63 | .sidebar li.selected{ 64 | border-left: 7px solid #42a5f5; 65 | } 66 | .sidebar li:hover { 67 | background-color: #2D4256; 68 | } 69 | .sidebar .pure-menu-link{ 70 | display: inline-block; 71 | padding-right: 2px; 72 | } 73 | .sidebar a:hover{ 74 | background-color: #2D4256; 75 | color: #42a5f5; 76 | } 77 | .sidebar li:focus, .sidebar a:focus{ 78 | background-color: #2D4256; 79 | border: 0; 80 | } 81 | .widget{ 82 | width: 400px; 83 | height: 2250px; 84 | background-color: #fff; 85 | margin: 20px; 86 | } 87 | .dashboard-content{ 88 | 89 | } 90 | .main{ 91 | margin-left: 190px; 92 | padding-top: 45px; 93 | } 94 | .main h2 { 95 | margin-bottom: 0; 96 | } 97 | .last-synced-msg{ 98 | float: right; 99 | background-color: yellow; 100 | color: #222; 101 | padding: 20px; 102 | height: 50px; 103 | margin: -30px -30px auto auto; 104 | } 105 | .last-synced-msg button{ 106 | height: 50px; 107 | } 108 | .clear{ 109 | clear: both; 110 | height: 40px; 111 | width: 100%; 112 | } 113 | .text-button{ 114 | text-decoration: none; 115 | font-size: 20px; 116 | color: #333; 117 | } 118 | .volume-detail-title{ 119 | font-size: 20px; 120 | margin-left: 20px; 121 | } 122 | .volumes-table{ 123 | margin-top: 10px; 124 | } 125 | .volume-detail-table{ 126 | margin-top: 10px; 127 | } 128 | .volumes-table tr{ 129 | cursor: pointer; 130 | } 131 | .volumes-table td{ 132 | padding: 5px 5px 5px 10px; 133 | } 134 | .content-box{ 135 | background-color: #fff; 136 | padding: 30px; 137 | margin-right: 50px; 138 | } 139 | .options-box{ 140 | float: right; 141 | padding: 20px; 142 | margin-top: 30px; 143 | border: 1px solid #eee; 144 | } 145 | .summary-box { 146 | padding: 20px; 147 | background-color: #eee; 148 | box-shadow: #bbb 2px 2px 5px; 149 | width: 250px; 150 | border-radius: 10px; 151 | margin-bottom: 20px; 152 | float: left; 153 | margin-right: 20px; 154 | } 155 | 156 | .box-value{ 157 | display:block; 158 | font-size: 40px; 159 | } 160 | table { 161 | /* border-spacing: 0; */ 162 | /* border-collapse: collapse; */ 163 | /* border: 1px solid #ddd; */ 164 | /* padding: 20px; */ 165 | /* margin: 20px; */ 166 | background-color: #fff; 167 | /* box-shadow: 5px 5px 15px #777; */ 168 | } 169 | 170 | .volumes-table tbody tr:hover{ 171 | background-color: #eee; 172 | } 173 | 174 | .volume-id, .node-id{ 175 | color: #555; 176 | } 177 | 178 | .volume-type-details{ 179 | color: #555; 180 | } 181 | 182 | .bricks-status-grid{ 183 | 184 | } 185 | .bricks-status-grid-col{ 186 | width: 20px; 187 | float: left; 188 | } 189 | 190 | .brick-status-grid-box { 191 | width: 100%; 192 | height: 20px; 193 | margin-bottom: 1px; 194 | } 195 | 196 | .grid-status-up { 197 | background-color: green; 198 | } 199 | 200 | .volume-state-down, .volume-state-up, .volume-state-partial, .volume-state-degraded, .volume-state-stopped, .peer-state-online, .brick-state-online, .volume-state-down, .peer-state-offline, .brick-state-offline { 201 | display: inline-block; 202 | padding:2px 10px; 203 | border-radius: 5px; 204 | } 205 | 206 | .volume-state-up, .peer-state-online, .brick-state-online { 207 | background-color: green; 208 | color: #fff; 209 | } 210 | 211 | .volume-state-down, .peer-state-offline, .brick-state-offline { 212 | background-color: #FF5370; 213 | color: #fff; 214 | } 215 | 216 | .volume-state-partial { 217 | background-color: #00fa9a; 218 | } 219 | 220 | .volume-state-degraded { 221 | background-color: #ff8c00; 222 | } 223 | 224 | .volume-state-stopped { 225 | background-color: gray; 226 | color: #fff; 227 | } 228 | 229 | .utilization-container{ 230 | width: 150px; 231 | height: 12px; 232 | background-color: #eee; 233 | } 234 | .utilization-value { 235 | height: 12px; 236 | } 237 | 238 | .utilization-text, .utilization-p-text{ 239 | color: #333; 240 | font-size: 13px; 241 | } 242 | 243 | .utilization-above-10, .utilization-above-20, .utilization-above-30, .utilization-above-40, .utilization-above-50, .utilization-above-60{ 244 | background-color: green; 245 | } 246 | 247 | .utilization-above-70 { 248 | background-color: #00fa9a; 249 | } 250 | 251 | .utilization-above-80 { 252 | background-color: #ff8c00; 253 | } 254 | 255 | .utilization-above-90 { 256 | background-color: red; 257 | } 258 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, 5 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 6 | "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, 8 | "db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, 10 | "ecto": {:hex, :ecto, "3.1.7", "fa21d06ef56cdc2fdaa62574e8c3ba34a2751d44ea34c30bc65f0728421043e5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, 11 | "ecto_sql": {:hex, :ecto_sql, "3.1.6", "1e80e30d16138a729c717f73dcb938590bcdb3a4502f3012414d0cbb261045d8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0 or ~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"}, 13 | "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"}, 14 | "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 17 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 18 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, 19 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, 20 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, 21 | "phoenix": {:hex, :phoenix, "1.4.9", "746d098e10741c334d88143d3c94cab1756435f94387a63441792e66ec0ee974", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, 22 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 23 | "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 24 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"}, 25 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.3.0", "ca2650021894785452f183d0d61e45823cb3991a72587da9140c597ac1e0d4c8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.9", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm"}, 26 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, 27 | "plug": {:hex, :plug, "1.8.2", "0bcce1daa420f189a6491f3940cc77ea7fb1919761175c9c3b59800d897440fc", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, 28 | "plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 29 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, 30 | "postgrex": {:hex, :postgrex, "0.15.0", "dd5349161019caeea93efa42f9b22f9d79995c3a86bdffb796427b4c9863b0f0", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, 31 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, 32 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, 33 | "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, 34 | "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, 35 | "tzdata": {:hex, :tzdata, "1.0.1", "f6027a331af7d837471248e62733c6ebee86a72e57c613aa071ebb1f750fc71a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 36 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, 37 | } 38 | -------------------------------------------------------------------------------- /assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | /* Includes some default style for the starter application. 2 | * This can be safely deleted to start fresh. 3 | */ 4 | 5 | /* Milligram v1.3.0 https://milligram.github.io 6 | * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /* General style */ 12 | h1{font-size: 3.6rem; line-height: 1.25} 13 | h2{font-size: 2.8rem; line-height: 1.3} 14 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} 15 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} 16 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} 17 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} 18 | 19 | .container{ 20 | margin: 0 auto; 21 | max-width: 80.0rem; 22 | padding: 0 2.0rem; 23 | position: relative; 24 | width: 100% 25 | } 26 | select { 27 | width: auto; 28 | } 29 | 30 | /* Alerts and form errors */ 31 | .alert { 32 | padding: 15px; 33 | margin-bottom: 20px; 34 | border: 1px solid transparent; 35 | border-radius: 4px; 36 | } 37 | .alert-info { 38 | color: #31708f; 39 | background-color: #d9edf7; 40 | border-color: #bce8f1; 41 | } 42 | .alert-warning { 43 | color: #8a6d3b; 44 | background-color: #fcf8e3; 45 | border-color: #faebcc; 46 | } 47 | .alert-danger { 48 | color: #a94442; 49 | background-color: #f2dede; 50 | border-color: #ebccd1; 51 | } 52 | .alert p { 53 | margin-bottom: 0; 54 | } 55 | .alert:empty { 56 | display: none; 57 | } 58 | .help-block { 59 | color: #a94442; 60 | display: block; 61 | margin: -1rem 0 2rem; 62 | } 63 | 64 | /* Phoenix promo and logo */ 65 | .phx-hero { 66 | text-align: center; 67 | border-bottom: 1px solid #e3e3e3; 68 | background: #eee; 69 | border-radius: 6px; 70 | padding: 3em; 71 | margin-bottom: 3rem; 72 | font-weight: 200; 73 | font-size: 120%; 74 | } 75 | .phx-hero p { 76 | margin: 0; 77 | } 78 | .phx-logo { 79 | min-width: 300px; 80 | margin: 1rem; 81 | display: block; 82 | } 83 | .phx-logo img { 84 | width: auto; 85 | display: block; 86 | } 87 | 88 | /* Headers */ 89 | header { 90 | width: 100%; 91 | background: #fdfdfd; 92 | border-bottom: 1px solid #eaeaea; 93 | margin-bottom: 2rem; 94 | } 95 | header section { 96 | align-items: center; 97 | display: flex; 98 | flex-direction: column; 99 | justify-content: space-between; 100 | } 101 | header section :first-child { 102 | order: 2; 103 | } 104 | header section :last-child { 105 | order: 1; 106 | } 107 | header nav ul, 108 | header nav li { 109 | margin: 0; 110 | padding: 0; 111 | display: block; 112 | text-align: right; 113 | white-space: nowrap; 114 | } 115 | header nav ul { 116 | margin: 1rem; 117 | margin-top: 0; 118 | } 119 | header nav a { 120 | display: block; 121 | } 122 | 123 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ 124 | header section { 125 | flex-direction: row; 126 | } 127 | header nav ul { 128 | margin: 1rem; 129 | } 130 | .phx-logo { 131 | flex-basis: 527px; 132 | margin: 2rem 1rem; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/gluster_dashboard/gluster.ex: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.Gluster do 2 | @moduledoc """ 3 | The Gluster context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias GlusterDashboard.Repo 8 | 9 | alias GlusterDashboard.Gluster.Peer 10 | 11 | @doc """ 12 | Returns the list of peers. 13 | 14 | ## Examples 15 | 16 | iex> list_peers() 17 | [%Peer{}, ...] 18 | 19 | """ 20 | def list_peers do 21 | Repo.all(Peer) 22 | end 23 | 24 | @doc """ 25 | Gets a single peer. 26 | 27 | Raises `Ecto.NoResultsError` if the Peer does not exist. 28 | 29 | ## Examples 30 | 31 | iex> get_peer!(123) 32 | %Peer{} 33 | 34 | iex> get_peer!(456) 35 | ** (Ecto.NoResultsError) 36 | 37 | """ 38 | def get_peer!(id), do: Repo.get!(Peer, id) 39 | 40 | @doc """ 41 | Creates a peer. 42 | 43 | ## Examples 44 | 45 | iex> create_peer(%{field: value}) 46 | {:ok, %Peer{}} 47 | 48 | iex> create_peer(%{field: bad_value}) 49 | {:error, %Ecto.Changeset{}} 50 | 51 | """ 52 | def create_peer(attrs \\ %{}) do 53 | %Peer{} 54 | |> Peer.changeset(attrs) 55 | |> Repo.insert() 56 | end 57 | 58 | @doc """ 59 | Updates a peer. 60 | 61 | ## Examples 62 | 63 | iex> update_peer(peer, %{field: new_value}) 64 | {:ok, %Peer{}} 65 | 66 | iex> update_peer(peer, %{field: bad_value}) 67 | {:error, %Ecto.Changeset{}} 68 | 69 | """ 70 | def update_peer(%Peer{} = peer, attrs) do 71 | peer 72 | |> Peer.changeset(attrs) 73 | |> Repo.update() 74 | end 75 | 76 | @doc """ 77 | Deletes a Peer. 78 | 79 | ## Examples 80 | 81 | iex> delete_peer(peer) 82 | {:ok, %Peer{}} 83 | 84 | iex> delete_peer(peer) 85 | {:error, %Ecto.Changeset{}} 86 | 87 | """ 88 | def delete_peer(%Peer{} = peer) do 89 | Repo.delete(peer) 90 | end 91 | 92 | @doc """ 93 | Returns an `%Ecto.Changeset{}` for tracking peer changes. 94 | 95 | ## Examples 96 | 97 | iex> change_peer(peer) 98 | %Ecto.Changeset{source: %Peer{}} 99 | 100 | """ 101 | def change_peer(%Peer{} = peer) do 102 | Peer.changeset(peer, %{}) 103 | end 104 | 105 | alias GlusterDashboard.Gluster.Volume 106 | 107 | @doc """ 108 | Returns the list of volumes. 109 | 110 | ## Examples 111 | 112 | iex> list_volumes() 113 | [%Volume{}, ...] 114 | 115 | """ 116 | def list_volumes do 117 | Repo.all(Volume) 118 | end 119 | 120 | @doc """ 121 | Gets a single volume. 122 | 123 | Raises `Ecto.NoResultsError` if the Volume does not exist. 124 | 125 | ## Examples 126 | 127 | iex> get_volume!(123) 128 | %Volume{} 129 | 130 | iex> get_volume!(456) 131 | ** (Ecto.NoResultsError) 132 | 133 | """ 134 | def get_volume!(id), do: Repo.get!(Volume, id) 135 | 136 | def get_volume_by_name(volname) do 137 | Repo.get_by(Volume, name: volname) 138 | |> Repo.preload([:subvols, [bricks: :peers], :options]) 139 | end 140 | 141 | @doc """ 142 | Creates a volume. 143 | 144 | ## Examples 145 | 146 | iex> create_volume(%{field: value}) 147 | {:ok, %Volume{}} 148 | 149 | iex> create_volume(%{field: bad_value}) 150 | {:error, %Ecto.Changeset{}} 151 | 152 | """ 153 | def create_volume(attrs \\ %{}) do 154 | %Volume{} 155 | |> Volume.changeset(attrs) 156 | |> Repo.insert() 157 | end 158 | 159 | @doc """ 160 | Updates a volume. 161 | 162 | ## Examples 163 | 164 | iex> update_volume(volume, %{field: new_value}) 165 | {:ok, %Volume{}} 166 | 167 | iex> update_volume(volume, %{field: bad_value}) 168 | {:error, %Ecto.Changeset{}} 169 | 170 | """ 171 | def update_volume(%Volume{} = volume, attrs) do 172 | volume 173 | |> Volume.changeset(attrs) 174 | |> Repo.update() 175 | end 176 | 177 | @doc """ 178 | Deletes a Volume. 179 | 180 | ## Examples 181 | 182 | iex> delete_volume(volume) 183 | {:ok, %Volume{}} 184 | 185 | iex> delete_volume(volume) 186 | {:error, %Ecto.Changeset{}} 187 | 188 | """ 189 | def delete_volume(%Volume{} = volume) do 190 | Repo.delete(volume) 191 | end 192 | 193 | @doc """ 194 | Returns an `%Ecto.Changeset{}` for tracking volume changes. 195 | 196 | ## Examples 197 | 198 | iex> change_volume(volume) 199 | %Ecto.Changeset{source: %Volume{}} 200 | 201 | """ 202 | def change_volume(%Volume{} = volume) do 203 | Volume.changeset(volume, %{}) 204 | end 205 | 206 | alias GlusterDashboard.Gluster.Subvol 207 | 208 | @doc """ 209 | Returns the list of subvols. 210 | 211 | ## Examples 212 | 213 | iex> list_subvols() 214 | [%Subvol{}, ...] 215 | 216 | """ 217 | def list_subvols do 218 | Repo.all(Subvol) 219 | end 220 | 221 | @doc """ 222 | Gets a single subvol. 223 | 224 | Raises `Ecto.NoResultsError` if the Subvol does not exist. 225 | 226 | ## Examples 227 | 228 | iex> get_subvol!(123) 229 | %Subvol{} 230 | 231 | iex> get_subvol!(456) 232 | ** (Ecto.NoResultsError) 233 | 234 | """ 235 | def get_subvol!(id), do: Repo.get!(Subvol, id) 236 | 237 | @doc """ 238 | Creates a subvol. 239 | 240 | ## Examples 241 | 242 | iex> create_subvol(%{field: value}) 243 | {:ok, %Subvol{}} 244 | 245 | iex> create_subvol(%{field: bad_value}) 246 | {:error, %Ecto.Changeset{}} 247 | 248 | """ 249 | def create_subvol(attrs \\ %{}) do 250 | %Subvol{} 251 | |> Subvol.changeset(attrs) 252 | |> Repo.insert() 253 | end 254 | 255 | @doc """ 256 | Updates a subvol. 257 | 258 | ## Examples 259 | 260 | iex> update_subvol(subvol, %{field: new_value}) 261 | {:ok, %Subvol{}} 262 | 263 | iex> update_subvol(subvol, %{field: bad_value}) 264 | {:error, %Ecto.Changeset{}} 265 | 266 | """ 267 | def update_subvol(%Subvol{} = subvol, attrs) do 268 | subvol 269 | |> Subvol.changeset(attrs) 270 | |> Repo.update() 271 | end 272 | 273 | @doc """ 274 | Deletes a Subvol. 275 | 276 | ## Examples 277 | 278 | iex> delete_subvol(subvol) 279 | {:ok, %Subvol{}} 280 | 281 | iex> delete_subvol(subvol) 282 | {:error, %Ecto.Changeset{}} 283 | 284 | """ 285 | def delete_subvol(%Subvol{} = subvol) do 286 | Repo.delete(subvol) 287 | end 288 | 289 | @doc """ 290 | Returns an `%Ecto.Changeset{}` for tracking subvol changes. 291 | 292 | ## Examples 293 | 294 | iex> change_subvol(subvol) 295 | %Ecto.Changeset{source: %Subvol{}} 296 | 297 | """ 298 | def change_subvol(%Subvol{} = subvol) do 299 | Subvol.changeset(subvol, %{}) 300 | end 301 | 302 | alias GlusterDashboard.Gluster.Option 303 | 304 | @doc """ 305 | Returns the list of options. 306 | 307 | ## Examples 308 | 309 | iex> list_options() 310 | [%Option{}, ...] 311 | 312 | """ 313 | def list_options do 314 | Repo.all(Option) 315 | end 316 | 317 | @doc """ 318 | Gets a single option. 319 | 320 | Raises `Ecto.NoResultsError` if the Option does not exist. 321 | 322 | ## Examples 323 | 324 | iex> get_option!(123) 325 | %Option{} 326 | 327 | iex> get_option!(456) 328 | ** (Ecto.NoResultsError) 329 | 330 | """ 331 | def get_option!(id), do: Repo.get!(Option, id) 332 | 333 | @doc """ 334 | Creates a option. 335 | 336 | ## Examples 337 | 338 | iex> create_option(%{field: value}) 339 | {:ok, %Option{}} 340 | 341 | iex> create_option(%{field: bad_value}) 342 | {:error, %Ecto.Changeset{}} 343 | 344 | """ 345 | def create_option(attrs \\ %{}) do 346 | %Option{} 347 | |> Option.changeset(attrs) 348 | |> Repo.insert() 349 | end 350 | 351 | @doc """ 352 | Updates a option. 353 | 354 | ## Examples 355 | 356 | iex> update_option(option, %{field: new_value}) 357 | {:ok, %Option{}} 358 | 359 | iex> update_option(option, %{field: bad_value}) 360 | {:error, %Ecto.Changeset{}} 361 | 362 | """ 363 | def update_option(%Option{} = option, attrs) do 364 | option 365 | |> Option.changeset(attrs) 366 | |> Repo.update() 367 | end 368 | 369 | @doc """ 370 | Deletes a Option. 371 | 372 | ## Examples 373 | 374 | iex> delete_option(option) 375 | {:ok, %Option{}} 376 | 377 | iex> delete_option(option) 378 | {:error, %Ecto.Changeset{}} 379 | 380 | """ 381 | def delete_option(%Option{} = option) do 382 | Repo.delete(option) 383 | end 384 | 385 | @doc """ 386 | Returns an `%Ecto.Changeset{}` for tracking option changes. 387 | 388 | ## Examples 389 | 390 | iex> change_option(option) 391 | %Ecto.Changeset{source: %Option{}} 392 | 393 | """ 394 | def change_option(%Option{} = option) do 395 | Option.changeset(option, %{}) 396 | end 397 | 398 | alias GlusterDashboard.Gluster.Brick 399 | 400 | @doc """ 401 | Returns the list of bricks. 402 | 403 | ## Examples 404 | 405 | iex> list_bricks() 406 | [%Brick{}, ...] 407 | 408 | """ 409 | def list_bricks do 410 | Repo.all(Brick) 411 | |> Repo.preload([:volumes, :peers]) 412 | end 413 | 414 | @doc """ 415 | Gets a single brick. 416 | 417 | Raises `Ecto.NoResultsError` if the Brick does not exist. 418 | 419 | ## Examples 420 | 421 | iex> get_brick!(123) 422 | %Brick{} 423 | 424 | iex> get_brick!(456) 425 | ** (Ecto.NoResultsError) 426 | 427 | """ 428 | def get_brick!(id), do: Repo.get!(Brick, id) 429 | 430 | @doc """ 431 | Creates a brick. 432 | 433 | ## Examples 434 | 435 | iex> create_brick(%{field: value}) 436 | {:ok, %Brick{}} 437 | 438 | iex> create_brick(%{field: bad_value}) 439 | {:error, %Ecto.Changeset{}} 440 | 441 | """ 442 | def create_brick(attrs \\ %{}) do 443 | %Brick{} 444 | |> Brick.changeset(attrs) 445 | |> Repo.insert() 446 | end 447 | 448 | @doc """ 449 | Updates a brick. 450 | 451 | ## Examples 452 | 453 | iex> update_brick(brick, %{field: new_value}) 454 | {:ok, %Brick{}} 455 | 456 | iex> update_brick(brick, %{field: bad_value}) 457 | {:error, %Ecto.Changeset{}} 458 | 459 | """ 460 | def update_brick(%Brick{} = brick, attrs) do 461 | brick 462 | |> Brick.changeset(attrs) 463 | |> Repo.update() 464 | end 465 | 466 | @doc """ 467 | Deletes a Brick. 468 | 469 | ## Examples 470 | 471 | iex> delete_brick(brick) 472 | {:ok, %Brick{}} 473 | 474 | iex> delete_brick(brick) 475 | {:error, %Ecto.Changeset{}} 476 | 477 | """ 478 | def delete_brick(%Brick{} = brick) do 479 | Repo.delete(brick) 480 | end 481 | 482 | @doc """ 483 | Returns an `%Ecto.Changeset{}` for tracking brick changes. 484 | 485 | ## Examples 486 | 487 | iex> change_brick(brick) 488 | %Ecto.Changeset{source: %Brick{}} 489 | 490 | """ 491 | def change_brick(%Brick{} = brick) do 492 | Brick.changeset(brick, %{}) 493 | end 494 | 495 | def get_counts() do 496 | %{ 497 | volumes: Repo.one(from v in "volumes", select: count(v.id)), 498 | peers: Repo.one(from p in "peers", select: count(p.id)), 499 | bricks: Repo.one(from b in "bricks", select: count(b.id)) 500 | } 501 | end 502 | 503 | @topic inspect(__MODULE__) 504 | 505 | def subscribe do 506 | Phoenix.PubSub.subscribe(GlusterDashboard.PubSub, @topic) 507 | end 508 | 509 | def broadcast_change(event) do 510 | Phoenix.PubSub.broadcast(GlusterDashboard.PubSub, @topic, {__MODULE__, event}) 511 | 512 | {:ok, %{}} 513 | end 514 | 515 | def delete_all do 516 | Repo.delete_all(Brick) 517 | Repo.delete_all(Option) 518 | Repo.delete_all(Subvol) 519 | Repo.delete_all(Volume) 520 | Repo.delete_all(Peer) 521 | end 522 | 523 | end 524 | -------------------------------------------------------------------------------- /test/live_view_demo/gluster_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GlusterDashboard.GlusterTest do 2 | use GlusterDashboard.DataCase 3 | 4 | alias GlusterDashboard.Gluster 5 | 6 | describe "peers" do 7 | alias GlusterDashboard.Gluster.Peer 8 | 9 | @valid_attrs %{address: "some address", state: "some state"} 10 | @update_attrs %{address: "some updated address", state: "some updated state"} 11 | @invalid_attrs %{address: nil, state: nil} 12 | 13 | def peer_fixture(attrs \\ %{}) do 14 | {:ok, peer} = 15 | attrs 16 | |> Enum.into(@valid_attrs) 17 | |> Gluster.create_peer() 18 | 19 | peer 20 | end 21 | 22 | test "list_peers/0 returns all peers" do 23 | peer = peer_fixture() 24 | assert Gluster.list_peers() == [peer] 25 | end 26 | 27 | test "get_peer!/1 returns the peer with given id" do 28 | peer = peer_fixture() 29 | assert Gluster.get_peer!(peer.id) == peer 30 | end 31 | 32 | test "create_peer/1 with valid data creates a peer" do 33 | assert {:ok, %Peer{} = peer} = Gluster.create_peer(@valid_attrs) 34 | assert peer.address == "some address" 35 | assert peer.state == "some state" 36 | end 37 | 38 | test "create_peer/1 with invalid data returns error changeset" do 39 | assert {:error, %Ecto.Changeset{}} = Gluster.create_peer(@invalid_attrs) 40 | end 41 | 42 | test "update_peer/2 with valid data updates the peer" do 43 | peer = peer_fixture() 44 | assert {:ok, %Peer{} = peer} = Gluster.update_peer(peer, @update_attrs) 45 | assert peer.address == "some updated address" 46 | assert peer.state == "some updated state" 47 | end 48 | 49 | test "update_peer/2 with invalid data returns error changeset" do 50 | peer = peer_fixture() 51 | assert {:error, %Ecto.Changeset{}} = Gluster.update_peer(peer, @invalid_attrs) 52 | assert peer == Gluster.get_peer!(peer.id) 53 | end 54 | 55 | test "delete_peer/1 deletes the peer" do 56 | peer = peer_fixture() 57 | assert {:ok, %Peer{}} = Gluster.delete_peer(peer) 58 | assert_raise Ecto.NoResultsError, fn -> Gluster.get_peer!(peer.id) end 59 | end 60 | 61 | test "change_peer/1 returns a peer changeset" do 62 | peer = peer_fixture() 63 | assert %Ecto.Changeset{} = Gluster.change_peer(peer) 64 | end 65 | end 66 | 67 | describe "volumes" do 68 | alias GlusterDashboard.Gluster.Volume 69 | 70 | @valid_attrs %{arbiter_count: 42, disperse_count: 42, disperse_redundancy_count: 42, health: "some health", inodes_total: 42, inodes_used: 42, name: "some name", num_bricks: 42, num_subvols: 42, replica_count: 42, size_total: 42, size_used: 42, state: "some state", transport: "some transport", type: "some type"} 71 | @update_attrs %{arbiter_count: 43, disperse_count: 43, disperse_redundancy_count: 43, health: "some updated health", inodes_total: 43, inodes_used: 43, name: "some updated name", num_bricks: 43, num_subvols: 43, replica_count: 43, size_total: 43, size_used: 43, state: "some updated state", transport: "some updated transport", type: "some updated type"} 72 | @invalid_attrs %{arbiter_count: nil, disperse_count: nil, disperse_redundancy_count: nil, health: nil, inodes_total: nil, inodes_used: nil, name: nil, num_bricks: nil, num_subvols: nil, replica_count: nil, size_total: nil, size_used: nil, state: nil, transport: nil, type: nil} 73 | 74 | def volume_fixture(attrs \\ %{}) do 75 | {:ok, volume} = 76 | attrs 77 | |> Enum.into(@valid_attrs) 78 | |> Gluster.create_volume() 79 | 80 | volume 81 | end 82 | 83 | test "list_volumes/0 returns all volumes" do 84 | volume = volume_fixture() 85 | assert Gluster.list_volumes() == [volume] 86 | end 87 | 88 | test "get_volume!/1 returns the volume with given id" do 89 | volume = volume_fixture() 90 | assert Gluster.get_volume!(volume.id) == volume 91 | end 92 | 93 | test "create_volume/1 with valid data creates a volume" do 94 | assert {:ok, %Volume{} = volume} = Gluster.create_volume(@valid_attrs) 95 | assert volume.arbiter_count == 42 96 | assert volume.disperse_count == 42 97 | assert volume.disperse_redundancy_count == 42 98 | assert volume.health == "some health" 99 | assert volume.inodes_total == 42 100 | assert volume.inodes_used == 42 101 | assert volume.name == "some name" 102 | assert volume.num_bricks == 42 103 | assert volume.num_subvols == 42 104 | assert volume.replica_count == 42 105 | assert volume.size_total == 42 106 | assert volume.size_used == 42 107 | assert volume.state == "some state" 108 | assert volume.transport == "some transport" 109 | assert volume.type == "some type" 110 | end 111 | 112 | test "create_volume/1 with invalid data returns error changeset" do 113 | assert {:error, %Ecto.Changeset{}} = Gluster.create_volume(@invalid_attrs) 114 | end 115 | 116 | test "update_volume/2 with valid data updates the volume" do 117 | volume = volume_fixture() 118 | assert {:ok, %Volume{} = volume} = Gluster.update_volume(volume, @update_attrs) 119 | assert volume.arbiter_count == 43 120 | assert volume.disperse_count == 43 121 | assert volume.disperse_redundancy_count == 43 122 | assert volume.health == "some updated health" 123 | assert volume.inodes_total == 43 124 | assert volume.inodes_used == 43 125 | assert volume.name == "some updated name" 126 | assert volume.num_bricks == 43 127 | assert volume.num_subvols == 43 128 | assert volume.replica_count == 43 129 | assert volume.size_total == 43 130 | assert volume.size_used == 43 131 | assert volume.state == "some updated state" 132 | assert volume.transport == "some updated transport" 133 | assert volume.type == "some updated type" 134 | end 135 | 136 | test "update_volume/2 with invalid data returns error changeset" do 137 | volume = volume_fixture() 138 | assert {:error, %Ecto.Changeset{}} = Gluster.update_volume(volume, @invalid_attrs) 139 | assert volume == Gluster.get_volume!(volume.id) 140 | end 141 | 142 | test "delete_volume/1 deletes the volume" do 143 | volume = volume_fixture() 144 | assert {:ok, %Volume{}} = Gluster.delete_volume(volume) 145 | assert_raise Ecto.NoResultsError, fn -> Gluster.get_volume!(volume.id) end 146 | end 147 | 148 | test "change_volume/1 returns a volume changeset" do 149 | volume = volume_fixture() 150 | assert %Ecto.Changeset{} = Gluster.change_volume(volume) 151 | end 152 | end 153 | 154 | describe "subvols" do 155 | alias GlusterDashboard.Gluster.Subvol 156 | 157 | @valid_attrs %{arbiter_count: 42, disperse_count: 42, disperse_redundancy_count: 42, health: "some health", num_bricks: 42, replica_count: 42, type: "some type"} 158 | @update_attrs %{arbiter_count: 43, disperse_count: 43, disperse_redundancy_count: 43, health: "some updated health", num_bricks: 43, replica_count: 43, type: "some updated type"} 159 | @invalid_attrs %{arbiter_count: nil, disperse_count: nil, disperse_redundancy_count: nil, health: nil, num_bricks: nil, replica_count: nil, type: nil} 160 | 161 | def subvol_fixture(attrs \\ %{}) do 162 | {:ok, subvol} = 163 | attrs 164 | |> Enum.into(@valid_attrs) 165 | |> Gluster.create_subvol() 166 | 167 | subvol 168 | end 169 | 170 | test "list_subvols/0 returns all subvols" do 171 | subvol = subvol_fixture() 172 | assert Gluster.list_subvols() == [subvol] 173 | end 174 | 175 | test "get_subvol!/1 returns the subvol with given id" do 176 | subvol = subvol_fixture() 177 | assert Gluster.get_subvol!(subvol.id) == subvol 178 | end 179 | 180 | test "create_subvol/1 with valid data creates a subvol" do 181 | assert {:ok, %Subvol{} = subvol} = Gluster.create_subvol(@valid_attrs) 182 | assert subvol.arbiter_count == 42 183 | assert subvol.disperse_count == 42 184 | assert subvol.disperse_redundancy_count == 42 185 | assert subvol.health == "some health" 186 | assert subvol.num_bricks == 42 187 | assert subvol.replica_count == 42 188 | assert subvol.type == "some type" 189 | end 190 | 191 | test "create_subvol/1 with invalid data returns error changeset" do 192 | assert {:error, %Ecto.Changeset{}} = Gluster.create_subvol(@invalid_attrs) 193 | end 194 | 195 | test "update_subvol/2 with valid data updates the subvol" do 196 | subvol = subvol_fixture() 197 | assert {:ok, %Subvol{} = subvol} = Gluster.update_subvol(subvol, @update_attrs) 198 | assert subvol.arbiter_count == 43 199 | assert subvol.disperse_count == 43 200 | assert subvol.disperse_redundancy_count == 43 201 | assert subvol.health == "some updated health" 202 | assert subvol.num_bricks == 43 203 | assert subvol.replica_count == 43 204 | assert subvol.type == "some updated type" 205 | end 206 | 207 | test "update_subvol/2 with invalid data returns error changeset" do 208 | subvol = subvol_fixture() 209 | assert {:error, %Ecto.Changeset{}} = Gluster.update_subvol(subvol, @invalid_attrs) 210 | assert subvol == Gluster.get_subvol!(subvol.id) 211 | end 212 | 213 | test "delete_subvol/1 deletes the subvol" do 214 | subvol = subvol_fixture() 215 | assert {:ok, %Subvol{}} = Gluster.delete_subvol(subvol) 216 | assert_raise Ecto.NoResultsError, fn -> Gluster.get_subvol!(subvol.id) end 217 | end 218 | 219 | test "change_subvol/1 returns a subvol changeset" do 220 | subvol = subvol_fixture() 221 | assert %Ecto.Changeset{} = Gluster.change_subvol(subvol) 222 | end 223 | end 224 | 225 | describe "options" do 226 | alias GlusterDashboard.Gluster.Opton 227 | 228 | @valid_attrs %{name: "some name", value: "some value"} 229 | @update_attrs %{name: "some updated name", value: "some updated value"} 230 | @invalid_attrs %{name: nil, value: nil} 231 | 232 | def opton_fixture(attrs \\ %{}) do 233 | {:ok, opton} = 234 | attrs 235 | |> Enum.into(@valid_attrs) 236 | |> Gluster.create_opton() 237 | 238 | opton 239 | end 240 | 241 | test "list_options/0 returns all options" do 242 | opton = opton_fixture() 243 | assert Gluster.list_options() == [opton] 244 | end 245 | 246 | test "get_opton!/1 returns the opton with given id" do 247 | opton = opton_fixture() 248 | assert Gluster.get_opton!(opton.id) == opton 249 | end 250 | 251 | test "create_opton/1 with valid data creates a opton" do 252 | assert {:ok, %Opton{} = opton} = Gluster.create_opton(@valid_attrs) 253 | assert opton.name == "some name" 254 | assert opton.value == "some value" 255 | end 256 | 257 | test "create_opton/1 with invalid data returns error changeset" do 258 | assert {:error, %Ecto.Changeset{}} = Gluster.create_opton(@invalid_attrs) 259 | end 260 | 261 | test "update_opton/2 with valid data updates the opton" do 262 | opton = opton_fixture() 263 | assert {:ok, %Opton{} = opton} = Gluster.update_opton(opton, @update_attrs) 264 | assert opton.name == "some updated name" 265 | assert opton.value == "some updated value" 266 | end 267 | 268 | test "update_opton/2 with invalid data returns error changeset" do 269 | opton = opton_fixture() 270 | assert {:error, %Ecto.Changeset{}} = Gluster.update_opton(opton, @invalid_attrs) 271 | assert opton == Gluster.get_opton!(opton.id) 272 | end 273 | 274 | test "delete_opton/1 deletes the opton" do 275 | opton = opton_fixture() 276 | assert {:ok, %Opton{}} = Gluster.delete_opton(opton) 277 | assert_raise Ecto.NoResultsError, fn -> Gluster.get_opton!(opton.id) end 278 | end 279 | 280 | test "change_opton/1 returns a opton changeset" do 281 | opton = opton_fixture() 282 | assert %Ecto.Changeset{} = Gluster.change_opton(opton) 283 | end 284 | end 285 | 286 | describe "bricks" do 287 | alias GlusterDashboard.Gluster.Brick 288 | 289 | @valid_attrs %{block_size: 42, device: "some device", fs: "some fs", inodes_total: 42, inodes_used: 42, mount_options: "some mount_options", path: "some path", pid: 42, port: 42, size_total: 42, size_used: 42, state: "some state"} 290 | @update_attrs %{block_size: 43, device: "some updated device", fs: "some updated fs", inodes_total: 43, inodes_used: 43, mount_options: "some updated mount_options", path: "some updated path", pid: 43, port: 43, size_total: 43, size_used: 43, state: "some updated state"} 291 | @invalid_attrs %{block_size: nil, device: nil, fs: nil, inodes_total: nil, inodes_used: nil, mount_options: nil, path: nil, pid: nil, port: nil, size_total: nil, size_used: nil, state: nil} 292 | 293 | def brick_fixture(attrs \\ %{}) do 294 | {:ok, brick} = 295 | attrs 296 | |> Enum.into(@valid_attrs) 297 | |> Gluster.create_brick() 298 | 299 | brick 300 | end 301 | 302 | test "list_bricks/0 returns all bricks" do 303 | brick = brick_fixture() 304 | assert Gluster.list_bricks() == [brick] 305 | end 306 | 307 | test "get_brick!/1 returns the brick with given id" do 308 | brick = brick_fixture() 309 | assert Gluster.get_brick!(brick.id) == brick 310 | end 311 | 312 | test "create_brick/1 with valid data creates a brick" do 313 | assert {:ok, %Brick{} = brick} = Gluster.create_brick(@valid_attrs) 314 | assert brick.block_size == 42 315 | assert brick.device == "some device" 316 | assert brick.fs == "some fs" 317 | assert brick.inodes_total == 42 318 | assert brick.inodes_used == 42 319 | assert brick.mount_options == "some mount_options" 320 | assert brick.path == "some path" 321 | assert brick.pid == 42 322 | assert brick.port == 42 323 | assert brick.size_total == 42 324 | assert brick.size_used == 42 325 | assert brick.state == "some state" 326 | end 327 | 328 | test "create_brick/1 with invalid data returns error changeset" do 329 | assert {:error, %Ecto.Changeset{}} = Gluster.create_brick(@invalid_attrs) 330 | end 331 | 332 | test "update_brick/2 with valid data updates the brick" do 333 | brick = brick_fixture() 334 | assert {:ok, %Brick{} = brick} = Gluster.update_brick(brick, @update_attrs) 335 | assert brick.block_size == 43 336 | assert brick.device == "some updated device" 337 | assert brick.fs == "some updated fs" 338 | assert brick.inodes_total == 43 339 | assert brick.inodes_used == 43 340 | assert brick.mount_options == "some updated mount_options" 341 | assert brick.path == "some updated path" 342 | assert brick.pid == 43 343 | assert brick.port == 43 344 | assert brick.size_total == 43 345 | assert brick.size_used == 43 346 | assert brick.state == "some updated state" 347 | end 348 | 349 | test "update_brick/2 with invalid data returns error changeset" do 350 | brick = brick_fixture() 351 | assert {:error, %Ecto.Changeset{}} = Gluster.update_brick(brick, @invalid_attrs) 352 | assert brick == Gluster.get_brick!(brick.id) 353 | end 354 | 355 | test "delete_brick/1 deletes the brick" do 356 | brick = brick_fixture() 357 | assert {:ok, %Brick{}} = Gluster.delete_brick(brick) 358 | assert_raise Ecto.NoResultsError, fn -> Gluster.get_brick!(brick.id) end 359 | end 360 | 361 | test "change_brick/1 returns a brick changeset" do 362 | brick = brick_fixture() 363 | assert %Ecto.Changeset{} = Gluster.change_brick(brick) 364 | end 365 | end 366 | end 367 | -------------------------------------------------------------------------------- /assets/css/pure-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v1.0.0 3 | Copyright 2013 Yahoo! 4 | Licensed under the BSD License. 5 | https://github.com/yahoo/pure/blob/master/LICENSE.md 6 | */ 7 | /*! 8 | normalize.css v^3.0 | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.pure-button:focus,a:active,a:hover{outline:0}.pure-table,table{border-collapse:collapse;border-spacing:0}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}.pure-button,input{line-height:normal}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}.pure-button,.pure-form input:not([type]),.pure-menu{box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend,td,th{padding:0}legend{border:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-content:flex-start;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u,.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block;zoom:1}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;zoom:1;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:transparent;background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{filter:alpha(opacity=90);background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;margin:0;border-radius:0;border-right:1px solid #111;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=tel],.pure-form input[type=color],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=text],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px}.pure-form input[type=color]{padding:.2em .5em}.pure-form input:not([type]):focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=text]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=checkbox]:focus,.pure-form input[type=radio]:focus{outline:#129FEA auto 1px}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input:not([type])[disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=text][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input:not([type]),.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=text],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-aligned .pure-help-inline,.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=tel],.pure-form input[type=color],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=text],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=tel],.pure-group input[type=color],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=text]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td,.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} 12 | --------------------------------------------------------------------------------