フリーランスのためのネットビジネス専門学校 ネットで独立開業を目指す人を応援
フリーランスのためのネットビジネス専門学校 ネットで独立開業を目指す人を応援

ajaxとは?【初心者向け】

はじめに

ウェブアプリケーションで実行するには時間がかかりすぎる処理は、cronやバッチ処理などの非同期処理と分離することがあります。cronにしてしまうとアプリケーションからコントロールが効かず、待つしかない状況に陥ります。
Rundeckに重たい処理を置いて単独で動く状況を用意しつつ、ウェブアプリケーションからRundeckのAPIを使いRundeck上のジョブを制御してみます。

使ったもの

rundeck-3.0.6-20180917.war (Open source windows版)

RundeckのAPIを使えるようにするまで

Rundeckは標準でRESTなAPIを装備しています。簡単な設定をするだけで使えるようになります。

API tokenを発行する

Rundeckにログインし右上のアイコンProfileから設定する。
image.png

+マークで新しいAPI Tokenを発行画面へ行き
image.png

必要項目を入れてAPI Tokenの発行をする
image.png

:warning: API tokenの有効期限を変更する
インストール直後の状態だとAPIのトークンは最大で30日間しか有効にできません。システムに組み込むには不便なので、30日以上指定可能にしたい場合には、以下の設定をしておきます。

rundeck-config.properties
rundeck.api.tokens.duration.max=1y

0にすると無制限になりますが、今回は1y(1年間)にしておきました。

Duration string indicating maximum lifetime of API Tokens. If unset, the value will be “30d” (30 days). Format: “##{ydhms}” (years, days, hours, minutes, seconds). If you want to disable the max expiration you can set it to 0 and create token with 0 duration that don’t expire.

rundeck.org/2.9.4/administration/configuration-file-reference.html#security

設定を変更しRundeckを再起動すると、365dまで設定できるようになっています。
image.png

ここまでで前準備完了です。API Tokenはメモしておきます。

API_token.
triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe

APIにアクセスしてみる

RundeckのAPIは、http://hostname:4440/api/Version/ でアクセスできます。呼び出しが簡単なsystem/infoにアクセスして動作確認してみます。hostnameとport番号は環境に合わせて書き換えてください。

URL
hostname:4440/api/2/system/info?authtoken=triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe

API Tokenはパラメータに付ける形か、ヘッダーに含める形かどちらかです。
rundeck.org/docs/api/index.html#authentication

ブラウザでアクセスしてみて、XMLのメッセージがつらつらと返ってくればOKです。

:warning:
RundeckにログインしているブラウザでAPIの確認するとNot Foundの画面が出ます。この場合はRundeckにログインしていない別のブラウザを使うか、コマンドラインベースのツール(curl, wgetなど)で確認してください。

image.png

APIにアクセスできる環境が整いました。

Rundeckをアプリケーションに組み込む準備

ここから本題。
オンラインで時間のかかる処理をバックエンドのRundeckに依頼し、結果を待つアプリケーションを想定します。

やること
1. Rundeckにジョブを作成(GUI)
2. ジョブが次にいつ動くのか確認(API)
3. Rundeck APIでジョブの状態確認(API)
4. Rundeck APIでジョブの起動(API)

jobを作成

RundeckのGUIにログインしジョブを作ります。ジョブ自体もAPIで作れますが、設定済みのジョブをウェブアプリケーションと連携することに絞ります。今回は、時間のかかるジョブをエミュレートするだけなので、「30秒waitして正常終了するジョブ」を作成しました。

Power Shellのsleepを呼ぶだけ
image.png

30分に1回動くように設定
image.png

多重起動は禁止
image.png

UUIDは最下部にあります。一度ジョブ作成を完了させた後にジョブ編集すると見えます。
image.png

UUID.
98d2c286-3165-403a-b2c0-90dddc3f2205

次に動く時間の確認

次回ジョブが動く時間をアプリケーション側から見えるようにします。時間が見えていれば待つべきか手動実行するべきか判断付きやすくなります。特定のジョブがRundeck側でいつ動くのか調べるには、job/infoを使います。

curl --verbose 
  -H 'Accept:application/json ' 
  -H 'X-Rundeck-Auth-Token:triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe'  
  http://hostname:4440/api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205/info

実際は1行で書きます

{
"href":"http://hostname:4440//api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205",
"averageDuration":21872,
"id":"98d2c286-3165-403a-b2c0-90dddc3f2205",
"scheduleEnabled":true,
"scheduled":true,
"enabled":true,
"nextScheduledExecution":"2018-09-29T16:30:00Z",
"permalink":"http://hostname:4440//project/API_test/job/show/98d2c286-3165-403a-b2c0-90dddc3f2205",
"group":null,
"description":"",
"project":"API_test",
"name":"job 30 second"
}

次回実行時刻は、nextScheduledExecutionに入っています。

rundeck.org/docs/api/index.html#get-job-metadata

今、ジョブが動いているか確認

アプリケーション側からジョブを動かす前に、そのジョブが動いているか確認する必要があります。複数人でブラウザを見ているケースや、Rundeckのスケジューラ側の起動とのバッティングを想定しています。

jobのUUIDを指定してexecutionsのAPIを実行すると取得できます。動いているジョブがあるか確認したいだけなのでstatus=runningで絞り込りをします。

curl
 -H 'Accept:application/json '
 -H 'X-Rundeck-Auth-Token:triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe' 
 http://hostname:4440/api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205/executions?status=running

ジョブが動いていないときレスポンス

{
"paging":{"count":0,"total":0,"offset":0,"max":20},
"executions":[]
}

ジョブが動いているときレスポンス

{
"paging":{"count":1, "total":1, "offset":0,"max":20},
"executions":[{
  "id":45,
  "href":
  "http://hostname:4440//api/27/execution/45",
  "permalink":"http://hostname:4440//project/API_test/execution/show/45",
  "status":"running",
  "project":"API_test",
  "executionType":"user",
  "user":"admin",
  "date-started":{"unixtime":1538237400000,"date":"2018-09-29T16:10:00Z"},
  "job":{
    "id":"98d2c286-3165-403a-b2c0-90dddc3f2205",
    "averageDuration":21872,
    "name":"job 30 second",
    "group":"",
    "project":"API_test",
    "description":"",
    "href":"http://hostname:4440//api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205",
    "permalink":"http://hostname:4440//project/API_test/job/show/98d2c286-3165-403a-b2c0-90dddc3f2205"
  },
  "description":"powershell sleep 30",
  "argstring":null
}]
}

このAPIを叩いて、countが0か確認すればよさそうです。

rundeck.org/docs/api/index.html#getting-executions-for-a-job

ジョブの起動

ウェブアプリケーション側からバッチジョブを実行したいときに、job/runを実行します。

curl
 -H 'Accept:application/json '
 -H 'X-Rundeck-Auth-Token:triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe' 
 -X POST
 http://hostname:4440/api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205/run

jobのrun はPOSTなので注意です。

起動に成功したときのレスポンス

{
"id":46,
"href":"http://hostname:4440//api/27/execution/46",
"permalink":"http://hostname:4440//project/API_test/execution/show/46",
"status":"running",
"project":"API_test",
"executionType":"user",
"user":"admin from api",
"date-started":{"unixtime":1538237862120,"date":"2018-09-29T16:17:42Z"},
"job":{
  "id":"98d2c286-3165-403a-b2c0-90dddc3f2205",
  "averageDuration":22907,
  "name":"job 30 second",
  "group":"",
  "project":"API_test",
  "description":"",
  "href":"http://hostname:4440//api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205",
  "permalink":"http://hostname:4440//project/API_test/job/show/98d2c286-3165-403a-b2c0-90dddc3f2205"
},
"description":"powershell sleep 30",
"argstring":null
}

起動に失敗した時のレスポンス

{
"error":true,
"apiversion":27,
"errorCode":"api.error.execution.conflict",
"message":"Execution had a conflict: Job "job 30 second"  is currently being executed "
}

rundeck.org/docs/api/index.html#running-a-job

アプリケーションに組み込む

HTMLで画面を作り、jQueryでRundeckのAPIを実行するものを作ってみました。

やっていること

10秒に一度、RundeckのAPIを実行し、次のジョブ実行時間、最終のジョブ実行時間、今動いているかを取ってくる。状態は画面に表示する。
ステータス確認ボタンは、10秒間隔で実行しているチェックを手動で実行するだけ。
ジョブ実行ボタンは、Rundeck側のジョブを呼び出し、時間のかかる処理をバックグラウンドで実行する
Rundeckのスケジュール時間まで5分を切ると、手動実行はできないようにする。

状態によって、ボタンの色(disabled)とアイコンが変わります。

平常時は手動実行も可能
image.png

実行中はボタンを押せない
image.png

スケジュール実行まで5分切ると手動実行は禁止
image.png

コード

rundeck-api.html
<html>
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
        <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
    </head>
    <body>
        <p>次のジョブ実行時間:<span id="next_execution">-</span></p>
        <p>最後のジョブ実行時間:<span id="last_execution">-</span></p>
        <p>チェック時間:<span id="last_check">-</span></p>
        <button id="b2" type="button" class="btn btn-primary" onclick="check()">ステータス確認</button>
        <button id="b3" type="button" class="btn btn-primary" onclick="executeJob()">
            <i class="far fa-play-circle" id="available" style='display:none'></i>
            <i class="far fa-pause-circle" id="not_available" style='display:none'></i>
            <i class="fas fa-sync fa-spin" id="running" style='display:none'></i> ジョブ実行</button>
    </body>

    <script>
        var jobid='98d2c286-3165-403a-b2c0-90dddc3f2205';
        var apitoken='triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe';
        var apiheader = {'X-Rundeck-Auth-Token':apitoken,'Accept':'application/json'};
        var running = false;
        var limit = 5;

        var next_execution_time = 0;
        var last_execution_time = 0;
        var last_check_time = 0;

        var job_interval = 0;
        var auto_check_interval = 10*1000; //sleep

        function check(){
            d = checkNextExecution();
            if( d == null ){
                checkJobStatus().then(checkLastExecution).then(setButtonStatus);
            }else{
                d.then(checkJobStatus).then(checkLastExecution).then(setButtonStatus);
            }
        }

        function checkNextExecution(){
            last_check_time = Date.now();
            if( next_execution_time > last_check_time ){
                return null;
            } 

            dfd = new $.Deferred;

            $.ajax({
                url:'/rundeck-api/27/job/' + jobid + '/info',
                type:'GET',
                dataType:'json',
                data:{},
                headers:apiheader
            })
            .done( (data) => {
                d = new Date(data.nextScheduledExecution);
                next_execution_time = d.getTime();
                console.log('next=%s, now=%s', next_execution_time, last_check_time);

                job_interval = (next_execution_time-last_check_time)/(60*1000);
                dfd.resolve();
            })
            .fail( (data) => {
                console.log(data);
                dfd.resolve();
            })
            .always( (data) => {
            });
            return dfd.promise();
        }

        function checkJobStatus(){
            dfd = new $.Deferred;

            $.ajax({
                url:'/rundeck-api/27/job/' + jobid + '/executions',
                type:'GET',
                dataType:'json',
                data:{'status':'running'},
                headers:apiheader
            })
            .done( (data) => {
                // console.log(data);
                running =  data.paging.count>0;
                dfd.resolve();
            })
            .fail( (data) => {
                console.log(data);
                dfd.resolve();
            })
            .always( (data) => {
            });
            return dfd.promise();
        }

        function checkLastExecution(){
            dfd = new $.Deferred;

            $.ajax({
                url:'/rundeck-api/27/job/' + jobid + '/executions',
                type:'GET',
                dataType:'json',
                data:{'max':'1'},
                headers:apiheader
            })
            .done( (data) => {
                last_execution_time = data.executions[0]['date-started'].unixtime;
                dfd.resolve();
            })
            .fail( (data) => {
                console.log(data);
                dfd.resolve();
            })
            .always( (data) => {
            });
            return dfd.promise();
        }

        function executeJob(){
            $.ajax({
                url:'/rundeck-api/27/job/' + jobid + '/run',
                type:'POST',
                dataType:'json',
                data:{},
                headers:apiheader
            })
            .done( (data) => {
                console.log(data);
                running = true;
                setButtonStatus();
            })
            .fail( (data) => {
                console.log(data);
            })
            .always( (data) => {
            });
        }

        function setButtonStatus(){
            console.log('interval=%s, limit=%s, running=%s',job_interval,limit,running);
            next_date = getFormattedDate(next_execution_time);
            last_date = getFormattedDate(last_execution_time);
            last_check = getFormattedDate(last_check_time);
            $('span#next_execution').text(next_date);
            $('span#last_execution').text(last_date);
            $('span#last_check').text(last_check);

            if( running ){
                $('i#available').hide();
                $('i#not_available').hide();
                $('i#running').show();
                $('button#b3').prop('disabled', true);
            }else{
                $('i#running').hide();

                if( job_interval < limit ){
                    $('i#available').hide();
                    $('i#not_available').show();
                    $('button#b3').prop('disabled', true);
                }else{
                    $('i#available').show();
                    $('i#not_available').hide();
                    $('button#b3').prop('disabled', false);
                }
            }
        }

        function getFormattedDate(time){
            date = new Date(time);
            y = date.getFullYear();
            m = padZ(date.getMonth()+1);
            d = padZ(date.getDate());
            h = padZ(date.getHours());
            mi = padZ(date.getMinutes());
            s = padZ(date.getSeconds());
            return y + '/' + m + '/' + d + ' ' + h + ':' + mi + ':' + s;
        }
        function padZ(a){
            if( a < 10 ){
                return '0' + a;
            }
            return a;
        }

        $(function(){
            check();
            setInterval(check,auto_check_interval);
        });
    </script>
</html>

おまけ

javascriptでRundeck APIへアクセスすると、CORSの設定がないためAPI接続エラーになります。Rundeckにbuild-inされているWebサーバーがAccess-Control-Allow-Originヘッダーを返していないので、ブラウザがRundeckへのリクエストを止めてしまいます。
ドキュメントを探してみたのですが、Access-Control-Allow-Originヘッダーを追加で設定するような項目は見つかりませんでした。

HTMLを配信したホストの/rundeck-api/以下にAPIのリクエストを送り、ApacheからProxyしてRundeckのポート付きのEndpointに転送する形です。

rundeck-api.conf
ProxyPass /rundeck-api/ http://hostname:4440/api/

まとめ

今回は、バックエンドにRundeckを置きウェブアプリアプリケーションの一部として組み込むサンプルを作りました。ウェブとバッチで処理を分離するとき、バッチの状態を管理するテーブルを作ったり、同時起動しないように配慮したりと、いろいろと頭を悩ます部分があると思いますが、ひとつの解決法になるのではないでしょうか。

[紹介元] jQueryタグが付けられた新着投稿 – Qiita ajaxとは?【初心者向け】

コメント

記事に戻る

コメントを残す