konchangakita

KPSを一番楽しんでいたブログ 会社の看板を背負いません 転載はご自由にどうぞ

【Nutanix UUIDエクスプローラーを作ってみよう】Next.js で 導入


React を CDN でぼちぼちJavaScriptしてきましたが、ここからフロントエンド部分を独立させ大きく開発環境を進化させます
フロントエンドのコンテナを別に作り、ReactベースのフロントエンドフレームワークNext.js を導入していきます

【Nutanix UUIDエクスプローラーを作ってみよう】シリーズ
REST API 〜 Flask表示
React で JavaScript 挑戦シリーズ

【Next.j 特訓】
・Next.js で導入←イマココ
Next.js環境に tailwindcss/ DaisyUI/ Font Awesomeを導入
ログインページをデザイン
フォームの実装
とりあえずFetchする
FetchからRouterでページ遷移
SSR で Fetch する
Layout を作る


今回の成果物

今回の最終形態はコチラ(Github)で公開しています
<フォルダ構造>


Next.js 用のコンテナの準備

とりあえず nodeベースでコンテナを作ります
これだけでOK
<dockerfile>

FROM node:latest
WORKDIR /usr/src/next-app/


Next.jsの導入

Next.js 日本語翻訳プロジェクトを参考にしながら、インストールしてみます

nodeバージョン確認
# node -v
v17.2.0
Next.js プロジェクト作成

プロジェクト名を指定しuuid-xplorerすることで、プロジェクト名のディレクトリが作成されて、必要なファイルたちが設置されます
今回はTypeScriptに挑戦したいので、--typescriptオプションをつけてインストールします

# npx create-next-app uuid-xplorer --typescript
Need to install the following packages:
  create-next-app
Ok to proceed? (y) 
自動的に作られたGitリポ削除

プロジェクト作成時に、自動的に Git initされているので、
コンテナごとまるっとGithub管理を統一したいので、自動的に作成されたプロジェクト配下の .git を消しちゃいます

# cd uuid-xplorer
# rm -rf .git

ポイントnode_modules 配下に大量にファイルが作られるので、.gitignoreで github 管理対象からはずしておきます
nodeでインストールすると、node_modules に関連ファイルが設置されると共に package.json にインストールした情報が追記されるので、コンテナの作成時に npm install を実行することで package.json内を参照してインストールし、同じ環境を作ることができます


Next.js インストールしたての package.json

{
  "name": "uuid-xplorer",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "12.1.4",
    "react": "18.0.0",
    "react-dom": "18.0.0"
  },
  "devDependencies": {
    "@types/node": "17.0.23",
    "@types/react": "17.0.43",
    "@types/react-dom": "17.0.14",
    "eslint": "8.12.0",
    "eslint-config-next": "12.1.4",
    "typescript": "4.6.3"
  }
}


docker-composeはこんな風に書いておけば、次回からは楽チン

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: frontend
    ports:
      - "7778:3000"
    volumes:
    - ./frontend/next-app:/usr/src/next-app:z
    command: bash -c "cd /usr/src/next-app/uuid-xplorer
      && npm install
      && tail -f /dev/null"
    networks:
      - uuid


Fast Refreshの有効

Next.jsではコード編集すると、stateを維持したまま変更が即時反映されます
これはめっちゃ便利な機能で、コイツの有無で開発スピードは断然違います
webpackDevMiddleware: config~部分を追記です
next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  webpackDevMiddleware: config => {
    config.watchOptions = {
      poll: 800,
      aggregateTimeout: 300,
    }
    return config
  },
}

module.exports = nextConfig


Next.jsで起動

uuid-explorerフォルダで以下を実行します

# npm run dev

これでサンプルサイトにアクセスできます
デフォルトでは、http://localhost:3000/
(今回の docker-composeの環境では http://localhost:7778/ でアクセスできるようにしています)


このページの内容は、uuid-explorer/pages/配下の
 _app.tsx
 index.tsx
になります

index.tsx をシンプルに編集してみます

import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        Welcome to UUID Xplorer
      </main>
    </div>
  )
}

export default Home

Fast Refreshが有効になっていれば、ファイルをセーブ後すぐに反映されるはずです


Next.js 準備完了

これでサクッと Next.js を実行できる環境ができました
あとは随時必要なモノが出てきたら、追加でインストールしていきます
次回、Webデザイン用に tailwindCSS, DaisyUI, fontawesomeをインストールしてみます

【Nutanix UUIDエクスプローラーを作ってみよう】React で JavaScript へ挑戦 - filter編


前回まで、CDNを使い fetchして取得したデータで state を更新して、表示というところまでを実装してきました
データ表示部分にリアルタイムでフィルタする動作を付け加えてみます
ようやく JavaScript 触ってるっぽい感じに


【Nutanix UUIDエクスプローラーを作ってみよう】シリーズ
Flask表示までのまとめ
React で JavaScript へ挑戦 - CDN導入編
React で JavaScript へ挑戦 - state編
React で JavaScript へ挑戦 - fetch編
・React で JavaScript へ挑戦 - filter編←イマココ

環境の準備

今回の最終形態はコチラ(Github)で公開しています
<フォルダ構造>


配列(リスト)を扱う

リストは map関数を使います、返り値も配列になります
配列のすべての要素に対して呼び出し、その結果からなる新しい配列を作って返します

vms.map((value, key) => {
  return (
    <div key={key}>
      {val}繰り返し処理で表示する内容
    </div>
  );
})

CDNで辞書型でキーを取り出したりはちょっとメンドそう)


試しにListクラス内で、サンプルの配列を作って map 使ってみるとこんな感じ(サンプルリストは UUID となんら関係ありません)

class List extends React.Component {
  render () {
    const labomen = ['kyoma', 'mayushi', 'daru', 'joshu', 'moeka', 'rukako', 'feilis', 'suzuha']
    const fglabo = labomen.map((member, key) => {
      return (
        <li key={key}>
          {member}
        </li>
      );
    })

    return (
      <div className="list">
        <ol>
          {fglabo}
        </ol>
      </div>
    );
  }
}


繰り返し処理される一番外の要素には key をつけてやらないと怒られます

フィルターしてみる

入力された文字列と一致する要素だけを表示するような、フィルターを実装してみたいと思います
filter関数を使うのが一般的なのかもしれませんが、CDN環境では使えなかったので、別の方法を使って入力されたキーワードにリアルタイムでフィルターしてみます

フィルターする方法として、「indexOf」を使ってみます

indexOf(filterText)

indexOf()は受け取ったテキストと一致する文字があれば、「インデックス番号」を、なければ「-1」を返します
これを使って、「-1」以外の要素を表示としてやります

文字が入力されるごとにレンダリングしてリアルタイムで表示を更新していきます(サンプルリストは UUID となんら関係ありません)

class List extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: ''
    };
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange = (event) => {
    this.setState( {[event.target.name]: event.target.value} )
  }

  render () {
    const labomen = ['kyoma', 'mayushi', 'daru', 'joshu', 'moeka', 'rukako', 'feilis', 'suzuha']
    const fglabo = labomen.map((member, key) => {
      if ( member.indexOf(this.state.filterText) !== -1 ) {
        return (
          <li key={key}>
            {member}
          </li>
        );
      }
      return;
    })

    return (
      <div className="list">
        <div><input type="text" placeholder="filter..." name="filterText" onChange={this.handleChange} /></div>
        <ol>
          {fglabo}
        </ol>
      </div>
    );
  }
}

今回は一致するものだけを表示というように作ってみましたが
例えば、一致した要素だけ色を変えたり表示を変えることもできるので、色々と応用できます

fetch してデータを map で取り出して filter する

これを実装することで、前回までの fetchして得られたデータの一覧表示とフィルタ機能のそれらしいものができました
/flaskr/static/js/test.js


さいごに

React を使うと 1ページで、色々できそうなことがみえてきました
ただ、Webサーバ側になんのインストールも必要ない超簡単実装できるCDNでは、できることの限界も見えてきたような気がします
次からは、ちゃんと環境構築して Next.js に挑戦します

【Nutanix UUIDエクスプローラーを作ってみよう】React で JavaScript へ挑戦 - fetch編

fetchって言葉は今回実際使ってみるまで、知らなかったのですが(Web初心者丸だし。。。)、Webページ上からFormなどから httpリクエストしてデータをひっぱってくるやつです

【Nutanix UUIDエクスプローラーを作ってみよう】シリーズ
Flask表示までのまとめ
React で JavaScript へ挑戦 - CDN導入編
React で JavaScript へ挑戦 - state編
・React で JavaScript へ挑戦 - fetch編 ←イマココ
React で JavaScript へ挑戦 - filter編

環境の準備

今回までの最終形態はコチラ(Github)で公開しています
<フォルダ構造>


JavaScript に fetch を導入

こんな感じで fetch 関数を呼び出してやることで httpリクエストしてやることができます

const response = await fetch("/api/connect", requestOptions);
// requestOptionsは他で設定する


Form で入力した値を使って httpリクエストする場合には、Form の onSubmit イベントから fetchをおこなう関数を呼び出してやります

<form onSubmit={this.handleConnectPrism} >

Form で受け取った Prism IP、ユーザ名、パスワードの state を使って、httpリクエストする関数はこんな感じ
/flaskr/static/js/test.js

  handleConnectPrism = async(event) => {
    event.preventDefault();
    const requestOptions = {
      method: "POST",
      headers: {'Content-Type' : 'application/json'},
      body: JSON.stringify({
        //Formから取得したデータ
        prism_ip: this.state.prismIp,
        prism_user: this.state.prismUser,
        prism_pass: this.state.prismPass,
      })
    }
    const response = await fetch("/api/connect", requestOptions);
    if (response.ok) {
      let res = await response.json();
      this.setState({
        // リクエスト結果を反映
      });
    }
    else {
      alert("HTTP-Error: " + response.status);
    }

ポイントは、非同期処理を行う async, await を使います
これを使わないと、httpリクエストの結果を待たず処理が進んでしまい、結果表示がおかしくなっちゃいます

state のリフトアップ

では、このfetchの関数をどこに設置するのか?です
React でクラスコンポーネントを使うときは、データの流れを親クラスから子クラスへというの意識する必要があります

今回の場合
1.3-1.FormConnect で、クラスタからデータ取得、Elasticsearchへデータ格納
2.3-2.FormDisplay で、Elasticsearch から表示用データ取得
3.4.List で、取得したデータを表示

この場合、この2つのクラスの親となる 2.Content に、stateをもたせて渡していくことになりますので、fetch関数もココに設置します
Contentクラスに配置させた場合の全体イメージは、こんなこんな感じ

</static/js/test.js>


httpリクエスト先の API作成

まだ現時点では、存在していない httpリクエスト先の 「/api/connect」、「/api/latestdataset」を作っていきます

基本的には Flaskで、API 受け口を作ってやるだけです
あとは以前から作っていたデータ取得の関数をよびだすだけ

from flask import make_response, jsonify

@app.route('/api/connect', methods=['POST'])
def connect():
    data = {}
    print(request.json)
    data['info'], data['cluster_name'] = connect_cluster(request.json)
    return make_response(jsonify(data))

@app.route('/api/latestdataset', methods=['POST'])
def latestdataset():
    cluster_name = request.json['cluster_name']
    data = get_dataset(cluster_name)
    return make_response(jsonify(data))



こうやって、Flaskを APIゲートウェイ的にどんどん作っていけます
ポイントは、Flaskからの返り値を json形式にしてやることで、javascriptで扱いやすくなります

return make_response(jsonify(data))


基本的には、追加していく API用のデコレータと関数は短めにして、共通関数を充実させていく感じですね
<app.py>

これで Elasticsearchにデータが入っている状態で、クラスタ名を入力し、表示ボタンを押してやれば、取得したデータが表示されます


React データ表示部分は

とりあえず表示していますが、React での Object 表示部分にはまだ触れていないので、また次回に

【Nutanix UUIDエクスプローラーを作ってみよう】React で JavaScript へ挑戦 - state編


ようやくここから JavaScript っぽいことをはじめていきます(JavaScriptよくわからないまま)

【Nutanix UUIDエクスプローラーを作ってみよう】シリーズ
Flask表示までのまとめ
React で JavaScript へ挑戦 - CDN導入編
・React で JavaScript へ挑戦 - state編 ←イマココ
React で JavaScript へ挑戦 - fetch編
React で JavaScript へ挑戦 - filter編

環境の準備

今回までの最終形態はコチラ(Github)で公開しています
<フォルダ構造>

関数コンポーネントとクラスコンポーネント

React ではユーザ定義したコンポーネントというのを組み合わせて、UIをつくっていきます
コンポーネントは、関数コンポーネントクラスコンポーネントというのがあります

くわしくは公式で コンポーネントと props – React

コンポーネントのレンダーにはこういうのん使います

<Element user='konchangakita' />

※ユーザ定義のコンポーネントは大文字で始めること


コンポーネント間でデータの受け渡しには Props というオブジェクトで渡すことができます
この例では、「name="konchangakita"」で渡して、「{props.name}」で呼び出しています
クラスコンポーネントProps の実装

class Element extends React.Component {
  constructor(props) {
    super(props);
  }

  render () {
    return (
      <div className="container">
        <h1>Hello, {this.props.user}</h1>
      </div>
    );
  }
}

ReactDOM.render(
  <Element user='konchangakita' />,
  document.getElementById('root')
);


ステート - React の状態管理

次に React の重要な要素であるステートを実装します
ステートは、現在の状態を表す変数のようなものです
React では初期値をクラスごとに「constructor」の中で「this.state」で宣言し、更新には状態を更新する用の関数「this.setState」を使います
Pythonでいうところの変数を更新していくのとはちょっと勝手が違い、はじめはちょっと戸惑います
公式はコチラ state とライフサイクル – React

ステート初期値の宣言

クラスの先頭で、「this.state」まずステートの初期値の宣言だけを行ってみます

class Element extends React.Component {
  constructor(props) {
    super(props);
    this.state = { words: "El psy congroo"}
  }

  render () {
    return (
      <div className="container">
        <h1>{this.state.words}</h1>
      </div>
    );
  }
}

ReactDOM.render(
  <Element />,
  document.getElementById('root')
);


Form の input でステートを使う

Form で input された文字列を取り扱うには、入力イベントをキャッチして、ステートを更新する関数を宣言してやる必要があります
input タグの valueで現在のステートonChangeイベントでステート更新の関数を呼び出します
公式はこちら フォーム – React

ステート更新

ステートの更新は 「this.setState」 を使います
input要素に入力があるごとに呼び出される onChangeイベントの関数の中で「this.setState」を使って更新していきます
単純に変数に代入するだけではダメなのです
伝統的に onXxxxxイベントで呼び出される関数は handleXxxxと書くらしい

入力しモノをコンソールに出すだけ

class Element extends React.Component {
  constructor(props) {
    super(props);
    this.state = { words: "" };
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    console.log(event.target.name, event.target.value);
    this.setState({ words: event.target.value });
  }

  render () {
    return (
      <div className="container">
        <form>
          <input type="text" name="words" value={this.state.words} onChange={this.handleChange}  />
        </form>
      </div>
    );
  }
}

ReactDOM.render(
  <Element />,
  document.getElementById('root')
);


console.log を置いておくと、デバッグに便利です
入力のたびにデベロッパーツールでコンソールに表示されます

ここまでの実装

公式のまとめに従い React の流儀 – React1つのコンポーネント1つの仕事だけさせるように、コンポーネントを分解していくことで、あとで機能を追加したり、メンテナンス性があがるらしい

というわけで、JavaScript 実装前の 前回まで作成していた Webページを コンポーネント単位に分解を考えます
データの流れを考えながら、/static/test.js をこんなイメージでコンポーネント単位で分解してみました

(3-1, 3-2で取得したデータを4で表示するために、2でデータを所持する。詳しくはまた次で)

/static/js/test.js

ちゃんと入力できているかのデバッグ確認はいくつかあると思いますが
console.logと合わせて React Developer Tools が分かりやすいと思います
React Developer Tools - Chrome ウェブストア

ブラウザのデベロッパーツールに機能が追加され、Reactコンポーネントの ステートがリアルタイムで表示されます



Reactを使う上では必須ツールと言えるでしょう

ステートを使いこなす

Reactのステート管理は長くなりがちでメンドウです
関数コンポーネントでは、フックを使っていくの結構楽になりますが、またあとの話になります
次回は、ステートで得た値を用いて fetch して→ データ取得 → ステート更新→ 表示を行います
ようやく Webアプリっぽくなってきます

【Nutanix UUIDエクスプローラーを作ってみよう】React で JavaScript へ挑戦 - CDN導入編

Flask の render template 機能で、Webページ表示というところまで作ってみました
次は、Webページの中身を JavaScript を使って表示というのにチャレンジです
ちな JavaScript は全くの初心者です

JavaScript の2大フレームワークとして、React と Vue というのある模様です
それぞれがどんな特徴か。。。というのはググればなんぼでもでてきますので、そちらにおまかせします
今回のUUIDエクスプローラープロジェクトでは、React で進めていきます
なんでかというと、信頼するエンジニアの方に「React の方がよいんじゃない?」と言っていただのがきっかけ、特になんの下調べもせずに飛び込んでみました


ja.reactjs.org

【Nutanix UUIDエクスプローラーを作ってみよう】シリーズ - React挑戦
Flask表示までのまとめ
・React で JavaScript へ挑戦 - CDN導入編 ←イマココ
React で JavaScript へ挑戦 - state編
React で JavaScript へ挑戦 - fetch編
React で JavaScript へ挑戦 - fetch編
React で JavaScript へ挑戦 - filter編


Reactどころか JavaScriptと初心者なので、チュートリアルを参考にしつつ、Getting Started をみながら実装を進めていきます
(日本語ページ助かる〜)

公式チュートリアルReact チュートリアル
チュートリアル:React の導入 – React

React Getting Started
Getting Started – React


環境の準備

今回の最終形態はコチラ(Github)で公開しています
<フォルダ構造>

今回のメインで扱うのは、js配下の test.js になります
(今回 Elasticは使いません)

React を CDN で導入する

とりあえずCDNで、React を使えるようにしてみます
Getting Started のこのあたりを参考にして、リンクをflaskで呼び出している index.html へ追加していきます
script タグを追加する
React で JSX を使う
JSXを使うことで、JavaScript独特な書き方をhtmlタグでわかりやすく書けるぽい

CDNリンク

こんな感じの CDN のリンクを追記します
/templates/index.html

    <!-- Reactをロード -->
    <script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
    <!-- JSXを使えるように -->
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    <!-- ReactのJavaScriptファイルの場所を指定 -->
    <script src="/static/js/test.js" type="text/babel"></script>  
    <!-- Reactをここまで -->


jsファイル作成

呼び出される側の JavaScript ファイルをシンプルにお試しで書いてみます
/static/js/test.js

const user = 'konchangakita';
const element = <h2>Hello, {user}</h2>;

ReactDOM.render(
  element,
  document.getElementById('root')
);


divタグで id指定

document.getElementById('root') で指定いる id を index.html で呼び出すだけです

<div id="root"></div>


index.htmlへの反映

CDNのリンクと合わせるこんな感じになります
./templates/index.html

      <h2>この下にReactする</h2>
      <div id="root"></div>

      <!-- Reactをロード -->
      <script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
      <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
      <!-- JSXを使えるように -->
      <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
      <!-- ReactのJavaScriptファイルの場所を指定 -->
      <script src="/static/js/test.js" type="text/babel"></script>  
      <!-- Reactをここまで -->


Webページの下の方で表示されるようになりました


jsファイルに html書いてみるだけ

では、Flask の render template機能で表示した部分と同じものを作ってみる
基本的にはそのまんまでいけます
/templates/index.html

/static/test.js

見かけ上は、上下見た目は同じなハリボテができました


とりあえずの CDN導入編

まだ JavaScript っぽいことは何もしてないハリボテができただけです、次から本番です
実際にデータ受け渡しなどなど、コンポーネント、state、fetch へ進みます

【Nutanix UUIDエクスプローラーを作ってみよう】Flask表示までのまとめ

しばらく時間があいてしまいましたが、UUIDエクスプローラーを作ってみようプロジェクト再開です

ここから、また長いみちのりになりそうです
ここまでは、Jupyter notebook 上で個別に実行しながら、Nutanix REST API 、Elasticsearch と Flask で表示までの動きを確認してきました
ここまでの全体像がどうなっているのかわかりにくかったので
ここで一度、復習がてらPythonの2つのプログラム(app.py, data_broker.py)にまとめて全体を晒しておきます

 ・app.py ・・・ ブラウザ表示、ゲートウェイ的な担当
 ・data_broker.py ・・・実データの入出力担当
  ※関数名を整理して変更しています

【Nutanix UUIDエクスプローラーを作ってみよう】シリーズ
REST APIしてみる
REST API 結果を Elasticsearch へ
Elasticsearch から Flask
・Flask表示までのまとめ イマココ
React で JavaScript へ挑戦 - CDN導入編

ディレクトリ全体像はこんな感じ
python直下のappフォルダいらないな。。。)

.
├── docker-compose.yml
├── elastic
│        └── es-data/この配下にelasticserchのデータがはいる
└── python
       ├── app
       │     └── flaskr
       │            ├── app.py
       │            ├── data_broker.py
       │            ├── static
       │            │     ├── image
       │            │     │     └── Nutanix_X_White.png
       │            │     └── style.css
       │            └── templates
       │                   └── index.html
       └── dockerfile

今回のゴール1.フォームでNutanixクラスタ情報を入力
2.REST API でデータ取得
3.Elasticsearch へデータ入力
4.FlaskでElasticsearch内の最新タイムスタンプの一覧を表示

実装コードたち

docker-compose.yml:Dockerまとめて制御するやつ
python/dockerfile:python環境のdocker
python/app/flaskr/app.py:Flask制御用
python/app/flaskr/data_broker.py:NutanixとElasticsearchデータ入出力python/app/flaskr/templates/index.html:Flaskテンプレート

まとめてこちら(Github)で公開
1.0307配下のdocker-compose -f "docker-compose.yml" up -d --buildを実行
2.pythonコンテナで、

> python app.py

3.ブラウザにて http://localhost:777にアクセス


docker-compose.yml

Elasticsearch と Kibina はそのまま使ってます

dockerfile

Jupyter Lab起動しているのは、検証しながら作っている名残り
最終的には、app.py を実行することになるでしょう

app.py

Webインターフェース
Flask レンダー機能を使って、index.html をテンプレートとして読み込んでいます
index.html で操作されたボタンによって動きを分岐させています


data_broker.py

データ入出力クラス
NutanixAPI:Nutanixクラスタからデータ取得するクラス
- 取得したいデータが増えてくるとここに追加
ElasticAPI:Elasticsearchとデータ入出力クラス
- REST APIで取得したデータ単位で index を作成し入力
- 検索用にエイリアスを作成

index.html

app.py で呼び出される Flask独特のテンプレートです
app.py からの変数を受け取り、ちょっとしたスクリプト(if や for)を使うことができます
取得した VM、Volume Gourpのリストを for文を使って、表示させています


Java Script へつづく

Flaskを使えば ローカルでWebサーバを立ち上げ、 Pythonで取得したデータを簡単に Webページで表示をすることができます

というわけで、
 ・Nutanixクラスタからデータ取得
 ・Elasticsearchでインプット、検索して表示
まではなんとなくできたので、ここからは Java Script を使って、もう少しリッチな Web UI 作る練習していきます

【Nutanix UUIDエクスプローラーを作ってみよう】Elasticsearch から Flask

【Nutanix Advent Calendar 2021】 11日目の記事です!

前回までは、Nutanix REST API からの Elasticsearch 投入までやったみましたので、次は簡単な Web GUI にチャレンジです

【Nutanix UUIDエクスプローラーを作ってみよう】シリーズ
REST APIしてみる
REST API 結果を Elasticsearch へ
・Elasticsearch から Flask イマココ
Flask表示までのまとめ

Flask で Web GUI 作成

今回やってみるイメージ
f:id:konchangakita:20220307185213p:plain


開発環境のおさらい
==開発環境====
VS Code
Jupyter
Docker

Nutanix
Python
Flask
Elasticsearch
Kibana
=============

Flaskの準備

基本的には前回のコンテナ環境をそのまま使います

こんな感じでコンテナ環境

dockerfile

FROM python:3.9.9-slim

RUN apt update -y
RUN apt install -y curl
RUN pip install -U pip
RUN pip install flask
RUN pip install jupyterlab
RUN pip install elasticsearch

#RUN apt install -y netcat
WORKDIR /home

# execute jpyterlab
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--allow-root", "--LabApp.token=''"]


docker-compose

version: '3.7'
services:
  es01:
    image: elasticsearch:7.14.2
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
    ports:
      - 9200:9200
      - 9300:9300
    volumes:
    - ./elastic/es-data:/usr/share/elasticsearch/data:z
    networks:
      - elastic

  kibana:
    image: kibana:7.14.2
    container_name: kibana
    ports:
      - 5601:5601
    networks:
      - elastic

  python:
    build:
      context: ./python
      dockerfile: Dockerfile
    container_name: python
    ports:
      - 1234:8888
      - 5678:5000
      - 777:777
    volumes:
    - ./python/app:/home/app:z
    networks:
      - elastic

networks:
  elastic:
    driver: bridge


構成ファイル

Flask構築用に flaskr ディレクトリを作って、2つの Pythonファイルで構成
・ app.py
変数を渡しつつレンダリング用の index.html を読み込む

・data_borker.py
データの入出力と整形用クラス・関数

全体のディレクトリ構成はこんな感じになっています

.
├── docker-compose.yml
├── elastic
│        └── es-data
└── python
           ├── app
           │         ├── api-elastic.ipynb
           │         └── flaskr
           │                     ├── app.py
           │                     ├── data_broker.py
           │                     ├── static
           │                     │         ├── image
           │                     │         └── style.css
           │                     └── templates
           │                                 ├── index.html
           │                                 └── layout.html
           └── dockerfile



Flask で Web表示

まずはシンプルに表示だけを試してみます
Flask設定用のapp.py を初期設定とport:777だけ設定してみます

app.py

from flask import Flask
from flask import render_template, request

import data_broker

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html', \
        title = 'Welcome to Prism')

if __name__  == '__main__':
    app.run(host="0.0.0.0", port=777, debug=True)


index.hml

{% extends "layout.html" %}
{% block body %}
<div class="container">
  <h1>{{title}}</h1>
</div>
{% endblock %}


タイトルだけを表示の状態
f:id:konchangakita:20211208020728p:plain
CSSは割愛)


Elasticsearchからデータ取得する

data_borker.py で Elasticsearch のクエリ結果を取得して、Flask を通して Web 表示してみたいと思います

【Elasticsearchから取得の流れ】
1.クラス名でタイムスタンプ一覧を取得
2.タイムスタンプとクラスタ名でクエリしてVMリストを取得

1.タイムスタンプ一覧を取得

app.py

from flask import Flask
from flask import render_template, request

import data_broker

app = Flask(__name__)

# set Elasticserch server
ELASTIC_SERVER = 'elasticsearch:9200'
es = data_broker.Ela(es_server=ELASTIC_SERVER)

@app.route('/')
def index():

    timeslot = es.get_timeslot('POC20')

    return render_template('index.html', \
        title = 'Welcome to Prism', \
        timeslot = timeslot        
        )

if __name__  == '__main__':
    app.run(host="0.0.0.0", port=777, debug=True)


data_broker.py

from elasticsearch import Elasticsearch

class Ela():
    def __init__(self, es_server):
        self.es = Elasticsearch(es_server)

    def get_timeslot(self, cluster_name):
        es = self.es
        index_name = 'vm_list'

        query = {
            "function_score" : {
                "query": {"match": { 'status.cluster_reference.name' : cluster_name}}
            }
        }
        aggs =  {
            "group_by_timestamp": {"terms": { "field" : "timestamp", "size" : 1000}}
        }
        res = es.search(index=index_name, query=query, aggs=aggs)
        _timeslot = [slot['key_as_string'] for slot in res['aggregations']['group_by_timestamp']['buckets']]
        timeslot = sorted(_timeslot, reverse=True)
        return timeslot


index.html

{% extends "layout.html" %}
{% block body %}
<div class="container">
  <h1>{{title}}</h1>
</div>
<div class="container">
  {{ timeslot }}
</div>
{% endblock %}

これでずらっとタイムスタンプが取得できました
f:id:konchangakita:20211210013247p:plain

2.タイムスタンプとクラスタ名でクエリ

取得したタイムスタンプを使って、vmリストを取得するクエリ作ります

app.py

from flask import Flask
from flask import render_template, request

import data_broker

app = Flask(__name__)

# set Elasticserch server
ELASTIC_SERVER = 'elasticsearch:9200'
es = data_broker.Ela(es_server=ELASTIC_SERVER)

@app.route('/')
def index():
    cluster_name = 'POC20'

    timeslot = es.get_timeslot(cluster_name)
    info = es.get_eslist('vm_list', timeslot[0], cluster_name)

    return render_template('index.html', \
        title = 'Welcome to Prism', \
        timeslot = timeslot, \
        cluster_name = cluster_name, \
        info = info
        )

if __name__  == '__main__':
    app.run(host="0.0.0.0", port=777, debug=True)


data_broker.py の Elaクラスに vmリストを取得する関数を追加

    def get_eslist(self, index_name, timestamp, cluster_name):
        es = self.es
        query =  {
            "function_score" : {
                "query": { "bool": { "must": [
                    {"match": { "timestamp" : timestamp}},
                    {"match": { "cluster_name" : cluster_name}}
                ]}}
            }
        }
        res = es.search(index=index_name, query=query, size=512)
        return [s['_source'] for s in res['hits']['hits'] ]



ずらずらと json形式で vmリストが取得できています
f:id:konchangakita:20211210025247p:plain


Flaskで整形する

json 形式の文字列から VM名とuuid だけを抜き出して、表示をすっきりさせてみましょう
Flaskのお作法に従って index.html を加工していってみます

index.html

{% extends "layout.html" %}
{% block body %}
<div class="container">
  <h1>{{title}}</h1>
  {% if info %}
    <div class="output-content">
      <h2>Cluster name: {{cluster_name}}</h2>
      {% for entity in info %}
        <div class="vm-name">{{ entity.spec.name }}</div> <div class="uuid">{{ entity.metadata.uuid }}</div><br clear="all">
      {% endfor %}
    </div>
  {% endif %}
</div>
{% endblock %}


これで良い感じに VM名と uuid の対応わかりやすく表示できました
f:id:konchangakita:20211210025914p:plain


ここからElasticsearch へのクエリを条件を工夫することで、uuid を検索し VM と ボリュームグループや他の関連する情報と紐付けて表示することが出来そうです
その為には、もっと色々と情報を取得して Elasticsearch へ突っ込んでおく必要がありますね
今回はここまで