IBM Cloud Object Storageを介したファイル転送 - その2 (curl編)

#はじめに
以前、[IBM Cloud Object Storageを介したファイル転送](https://qiita.com/tomotagwork/items/94e9974efb6249f50836)という記事を書きました。この記事では主にAWS CLIを使ってICOSとファイル送受信をするやり方について記載しました。

IBM Cloudでは、Intel系のサーバー(WindowsやLinux)だけでなくPower System(AIX, IBMi)の仮想サーバー環境も提供しています。
参考: [IBM Power Systems Virtual Server](https://cloud.ibm.com/docs/power-iaas?topic=power-iaas-getting-started)
今後IBM Cloud上の仮想サーバーとしてAIXを使用する予定があるのですが、はて、AWS CLIってAIXで使えるのかな？と思ってみてみると、やっぱりAIX版なんてありません。
参考: [AWS CLI バージョン 2 のインストール、更新、アンインストール](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv2.html)

ということで、AIXでも使えるようになるべく汎用的な方法、つまりREST API(curl)でアクセスする方法について試してみました。
参考:[IBM Cloud Object Storage S3 API について](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-compatibility-api)

※とはいえ、現時点で自由に使えるIBM Cloud上にAIX環境がある訳では無いので、ローカルのLinux上のbash/kshで動作確認をしています。恐らくそのままAIXでも使えるんじゃなかろうかと...

※後々気づいたのですが、AWS CLIのV2だと専用のインストーラーが提供されているようだったのでAIXで使えないと思っていたのですが、AWS CLI V1はPythonのpipでインストールできるのでAIXでも同様にいけるっぽいです。V1, V2で何が違うか分かりませんが、そっちの方が楽かも。
参考: [pip を使用した AWS CLI バージョン 1 のインストールとアンインストール](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-linux.html#install-linux-pip)
## 関連記事

[IBM Cloud Object Storageを介したファイル転送](https://qiita.com/tomotagwork/items/94e9974efb6249f50836)
[IBM Cloud Object Storageを介したファイル転送 - その2(curl編)](https://qiita.com/tomotagwork/items/a449e8a5d58360d05ec3)

# AIX Toolbox for Linux
今回curlでICOSにアクセスすることを考えると、JSONとかXMLをハンドリングしないといけません。この記事で試しているスクリプトでは、curl, jq, xmllintあたりを使用しています。
最近はめっきりAIX使う機会が減ってきており、はて、Linuxで当たり前のように使っているcurlとかjqとか使えるんだっけ？という疑問が湧いてきました。
AIX Toolbox for Linuxというのがあって、ここから入手できるようです(curl, jq, libxml2)。
参考: [AIX Toolbox for Linux Applications](https://www.ibm.com/support/pages/aix-toolbox-linux-applications-overview)
そういえばAIX版のRPMパッケージマネージャーみたいなものがあったんでしたね(完全に忘れてた...)。

(一応Linuxのkshでも確認してますが、上のToolboxにはbashもあるのでbash入れちゃうのもありかも。)

# curlによるICOSアクセス
さて、ここからが本題です。ICOS自体の作成方法などは前の記事をご参照ください。
ここでは、curlによるファイルの送受信を試してみます。素でcurlコマンドを打つのは面倒なので、簡素化するためのシェル・スクリプトをいくつか作成しています。

基本的には以下の記述に従います。
参考: [curlの使用](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-curl)

大まかな流れとしては以下のような感じです。

- 事前にICOSアクセス用の資格情報の設定をしておく([サービス資格情報の設定](https://qiita.com/tomotagwork/items/94e9974efb6249f50836#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E8%B3%87%E6%A0%BC%E6%83%85%E5%A0%B1%E3%81%AE%E8%A8%AD%E5%AE%9A))
- 上の資格情報のapikeyを元にアクセストークン(IAMトークン)を取得
- アクセストークンを使用してICOSへの各種アクセス操作を行う

## アクセストークンの取得
参考: [API キーを使用した IBM Cloud IAM トークンの生成](https://cloud.ibm.com/docs/account?topic=account-iamtoken_from_apikey)
上の例のように、ICOSのサービス資格情報を作成した時にアサインされたapikeyを使用して、アクセストークンを取得することができます。

アクセストークン取得用スクリプト

```sh:getAccessToken.sh
#!/bin/sh

### Input parameters
if [[ $# -lt 1 ]]
then
        echo Error: invalid arguments
        echo "  Usage: " `basename $0` "<apikey>"
        echo "  Example: " `basename $0` "SxyXXXXXXXXXXXXXXXXX"
        exit 1
fi

apikey=$1
uriAll="https://iam.cloud.ibm.com/identity/token?grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey=${apikey}"

### get AccessToken
result=$(curl -sS -X "POST" "${uriAll}" -H "Content-Type: application/x-www-form-urlencoded" -H "Accept: application/json")
rc=$?

if [[ ${rc} -gt 0 ]]
then
        exit 1
fi

#echo result: ${result}
errorCode=$(echo ${result} | jq .errorCode)

if [[ ${errorCode} != "null" ]]
then
        echo errorCode: ${errorCode}
        errorMessage=$(echo ${result} | jq .errorMessage)
        echo errorMesage: ${errorMessage}
        exit 1

fi

access_token=$(echo ${result} | jq .access_token)

if [[ ${access_token} != "null" ]]
then
        echo ${access_token}
        exit 0
else
        unset access_token
        exit 1
fi
```

apikeyを第1引数に指定してスクリプト実行すると、標準出力にアクセストークンが返されます。

```xxx:実行例
# ./getAccessToken.sh SxXXXXXXXX
"eyJraWQiOiIy... <中略> .....-S1M6MSXr2sUqFuR7RDnTUkAghMInw"
```

内部的には、以下のようなcurlコマンドを発行しており、そのうちのaccess_token部分を拾って標準出力に出すようにしています。

```xxx:curl実行例
# curl -sS -X "POST" "https://iam.cloud.ibm.com/identity/token?grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey=XXXXX" -H "Content-Type: application/x-www-form-urlencoded" -H "Accept: application/json" | jq .
{
  "access_token": "eyJraWQiOiIy... <中略> .....-S1M6MSXr2sUqFuR7RDnTUkAghMInw",
  "refresh_token": "OKDBFStiLrIu... <中略> .....-HORjedX2A2WSo4u0jsYJy2MrXJOHeYuBw",
  "token_type": "Bearer",
  "expires_in": 3600,
  "expiration": 1602912855,
  "refresh_token_expiration": 1605501255,
  "scope": "ibm openid"
}
```

access_tokenとして返される文字列は結構長いです。1239文字ありました。
これ以降のICOSアクセスには、このaccess_tokenを指定することになりますが、このアクセストークンは利用期限があり、3600秒(1時間)で使えなくなります。取得後1時間経過したら再度取得しなおす必要があります。

さて、このアクセストークンを手動でコピペしたりして後続処理を行うのは面倒なので、環境変数にセットできるような仕組みを用意します。ついでに、ICOSアクセス時に必要なエンドポイント、アクセスしたいICOSのインスタンスIDなども合わせて変数化しておくことにします。

まず、env.shというのを作って、そこに事前に必要な情報をセットします。

```sh:env.sh
#!/bin/sh

export icosAccessPoint="s3.jp-tok.cloud-object-storage.appdomain.cloud"
export instanceID="51xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxf6"
export apikey="xxxxxxxxxxxx"
```

icosAccessPointには、以下で確認したエンドポイントを設定します。
参考: [エンドポイント情報の確認](https://qiita.com/tomotagwork/items/94e9974efb6249f50836#%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E6%83%85%E5%A0%B1%E3%81%AE%E7%A2%BA%E8%AA%8D)

instanceIDとapikeyの値は、サービス資格情報から判断して設定します。
参考: [サービス資格情報の設定](https://qiita.com/tomotagwork/items/94e9974efb6249f50836#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E8%B3%87%E6%A0%BC%E6%83%85%E5%A0%B1%E3%81%AE%E8%A8%AD%E5%AE%9A)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/adb5a8f7-e253-9464-59d6-07403fd4430c.png)

次に、env.shとgetAccessToken.shを使ってアクセストークンを環境変数にセットするスクリプトを用意します。

```sh:setToken.sh
#!/bin/sh

. ./env.sh

access_token=$(./getAccessToken.sh ${apikey})
rc=$?

if [[ ${rc} -gt 0 ]]
then
        echo Error!
        echo ${access_token}
        unset access_token
else
        echo "access_token is set!"
        export access_token=$(echo ${access_token} | tr -d \" )

fi
```

これを実行するときは、実行したシェルで環境変数が有効になるように、先頭に「.」ドットを付けて、`. ./setToken.sh` というように現行シェル配下でスクリプトを実行するようにしてください。(sourceコマンド使おうと思ったらAIXに無さそうだったので...)

```xxx:実行例
# . ./setToken.sh
access_token is set!

# echo ${access_token}
eyJra ...<中略>... AGg
```

setToken.sh実行後、`echo ${access_token}`でトークンが表示されていればOKです。以降、ICOSアクセスはこの環境変数を使用するので、setToken.shを実行したシェルで後続処理を実行すればよいです。

つまり、env.shに必要な情報をセットし、`. ./setToken.sh`を実行すれば、トークンを直接意識しなくても後続のICOSアクセス処理が行えます。
(１時間経過してアクセスできなくなったら、再度setToken.shを実行すればOK)

## ICOSアクセス
setToken.shでICOSアクセス用のトークンが取得できたら、各種操作をやってみます。それぞれ簡易的なスクリプトを用意しました。
以下、いずれもsetToken.shでaccess_tokenをセットしたシェル上で実行する前提です。

### Bucketリスト取得

Bucketのリストを取得するためのスクリプトです。

```sh:getBucketList.sh
#!/bin/sh

uriAll="https://${icosAccessPoint}"

### get Object List
curl -sS -X "GET" ${uriAll} -H "Authorization: bearer ${access_token}" -H "ibm-service-instance-id: ${instanceID}" | xmllint --format -
```

```xxx:実行例
# ./getBucketList.sh
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Owner>
    <ID>51xxxf6</ID>
    <DisplayName>514xxxf6</DisplayName>
  </Owner>
  <Buckets>
    <Bucket>
      <Name>ise-tag-test01</Name>
      <CreationDate>2020-04-16T23:42:42.364Z</CreationDate>
    </Bucket>
  </Buckets>
</ListAllMyBucketsResult>
```

上の例では"ise-tag-test01"というBucketが1つのみ存在していることが分かります。
結果はXMLで返されますのでちょっと見にくいですが、面倒なのでここの出力の加工はしていません。

### Object(File)リスト取得

Bucketを指定して、Bucketに含まれるオブジェクト(File)のリストを表示するスクリプトです。XMLで返されるとここは分かりにくいので簡易的に加工して出しています(ファイル名、更新日時、サイズのみ出力)。

```sh:
#!/bin/sh

### Input parameters
if [[ $# -lt 1 ]]
then
        echo Error: invalid arguments
        echo "  Usage: " `basename $0` "<bucketName>"
        echo "  Example: " `basename $0` "bucket01"
        exit 1
fi

bucketName=$1

uriAll="https://${icosAccessPoint}/${bucketName}"

### get Object List
#curl -sS -X "GET" ${uriAll} -H "Authorization: bearer ${access_token}" -H "ibm-service-instance-id: ${instanceID}" | xmllint --format -

result=$(curl -sS -X "GET" ${uriAll} -H "Authorization: bearer ${access_token}" -H "ibm-service-instance-id: ${instanceID}")
rc=$?

if [[ ${rc} -gt 0 ]]
then
        echo curl error!
        exit 1
fi

error=$(echo ${result} | grep "<Error>")
rc=$?

if [[ ${rc} -eq 0 ]]
then
        echo access error!
        echo ${result} | xmllint --format -
        exit 1
fi

echo -e "Key\t\tLastModified\t\t\tSize"
echo -------------------------------------------------------------------
echo ${result} | xmllint --xpath "//*[local-name()='Contents']" - | sed -e "s/<ETag>[^<]*<\/ETag>//g" -e "s/<ID>[^<)]*<\/ID>//g"  -e "s/<DisplayName>[^<)]*<\/DisplayName>//g" -e "s/<StorageClass>[^<)]*<\/StorageClass>//g" -e "s/<\/*Owner>//g" -e "s/<Contents>//g" -e "s/<\/Contents>/
\n/g" -e "s/<[^/<]*>//g" -e "s/<\/[^<]*>/\t/g"
```

```xxx:実行例
# ./getObjectList.sh ise-tag-test01
Key             LastModified                    Size
-------------------------------------------------------------------
shell.tar.z     2020-10-15T07:56:40.602Z        1088
test2.txt       2020-10-16T09:51:55.393Z        5
test3.txt       2020-10-16T11:02:14.680Z        6
```

上の例だと、ise-tag-test01というbucket内に、3つのファイルがあるのが確認できます。

### Fileのアップロード

指定したbucketにファイルをアップロードするためのスクリプトです。bucketの直下に同名でファイルを転送します。アップロード対象のローカルのファイル名は相対パスでも絶対パスでもOKです。

```sh:putFile.sh
#!/bin/sh

### Input parameters
if [[ $# -lt 2 ]]
then
        echo Error: invalid arguments
        echo "  Usage: " `basename $0` "<bucketName> <fileName>"
        echo "  Example: " `basename $0` "bucket01 test.txt"
        exit 1
fi

bucketName=$1
sourceFileName=$2
targetFileName=$(echo ${sourceFileName} | sed -e "s/.*\///")

uriAll="https://${icosAccessPoint}/${bucketName}/${targetFileName}"

### put file
curl -sS -X "PUT" ${uriAll} -H "Authorization: bearer ${access_token}" -H "ibm-service-instance-id: ${instanceID}" --data-binary @${sourceFileName}
```

```xxx:実行例
# ./putFile.sh
Error: invalid arguments
  Usage:  putFile.sh <bucketName> <fileName>
  Example:  putFile.sh bucket01 test.txt

# ./putFile.sh ise-tag-test01 /root/ICOS/test4.txt

# ./getObjectList.sh ise-tag-test01
Key             LastModified                    Size
-------------------------------------------------------------------
shell.tar.z     2020-10-15T07:56:40.602Z        1088
test2.txt       2020-10-16T09:51:55.393Z        5
test3.txt       2020-10-16T11:02:14.680Z        6
test4.txt       2020-10-17T05:29:01.684Z        6
```

test4.txtというファイルが転送されたことが分かります。

Bucket直下ではなく特定のディレクトリ配下にアップロードしたい場合は、第1引数のBucket名に`ise-tag-test01/dir1`というように配置したいディレクトリも指定すればOKです。

### Fileのダウンロード

bucket直下にあるファイルをダウンロードするためのスクリプトです。
bucket名、ファイル名、ダウンロード先ディレクトリを指定します。

```sh:getFile.sh
#!/bin/sh

### Input parameters
if [[ $# -lt 3 ]]
then
        echo Error: invalid arguments
        echo "  Usage: " `basename $0` "<bucketName> <fileName> <outputDirName>"
        echo "  Example: " `basename $0` "bucket01 test.txt outDir"
        exit 1
fi

bucketName=$1
sourceFileName=$2
outputDirName=$3
targetFileName=$(echo ${sourceFileName} | sed -e "s/.*\///")


uriAll="https://${icosAccessPoint}/${bucketName}/${sourceFileName}"

### get file
curl -sS -X "GET" ${uriAll} -H "Authorization: bearer ${access_token}" -H "ibm-service-instance-id: ${instanceID}" > ${outputDirName}/${targetFileName}
```

```xxx:実行例
# ./getFile.sh
Error: invalid arguments
  Usage:  getFile.sh <bucketName> <fileName> <outputDirName>
  Example:  getFile.sh bucket01 test.txt outDir

# ./getFile.sh ise-tag-test01 test4.txt outDir

# ls -la outDir/
合計 12
drwxr-xr-x. 2 root root   23 10月 17 14:32 .
drwxr-xr-x. 3 root root 4096 10月 17 14:28 ..
-rw-r--r--. 1 root root    6 10月 17 14:32 test4.txt
```

test4.txtファイルがoutDirにダウンロードされたことが分かります。

Bucket直下ではなく特定のディレクトリ配下にあるファイルをダウンロードしたい場合は、第2引数のファイル名に`dir1/test4.txt`というようにディレクトリ付で指定すればOKです。

### Fileの削除

ICOS上のファイルを削除するためのスクリプトです。

```sh:deleteFile.sh
#!/bin/sh

### Input parameters
if [[ $# -lt 2 ]]
then
        echo Error: invalid arguments
        echo "  Usage: " `basename $0` "<bucketName> <fileName>"
        echo "  Example: " `basename $0` "bucket01 test.txt"
        exit 1
fi

bucketName=$1
fileName=$2

uriAll="https://${icosAccessPoint}/${bucketName}/${fileName}"

### download file
curl -sS -X "DELETE" ${uriAll} -H "Authorization: bearer ${access_token}" -H "ibm-service-instance-id: ${instanceID}"
```

```xxx:実行例
# ./deleteFile.sh
Error: invalid arguments
  Usage:  deleteFile.sh <bucketName> <fileName>
  Example:  deleteFile.sh bucket01 test.txt

# ./getObjectList.sh ise-tag-test01
Key             LastModified                    Size
-------------------------------------------------------------------
shell.tar.z     2020-10-15T07:56:40.602Z        1088
test2.txt       2020-10-16T09:51:55.393Z        5
test3.txt       2020-10-16T11:02:14.680Z        6
test4.txt       2020-10-17T05:29:01.684Z        6

# ./deleteFile.sh ise-tag-test01 test4.txt

# ./getObjectList.sh ise-tag-test01
Key             LastModified                    Size
-------------------------------------------------------------------
shell.tar.z     2020-10-15T07:56:40.602Z        1088
test2.txt       2020-10-16T09:51:55.393Z        5
test3.txt       2020-10-16T11:02:14.680Z        6
```

bucketからtest4.txtというファイルが削除されたことが分かります。

##補足
ファイルのアップロード、ダウンロードについては、テキストファイルとtarで固められたバイナリファイルで確認しています。tarで固められたファイルをICOSにアップロード/ダウンロードし、その後解凍できることを確認しました。


#おわりに
これで、ICOSアクセス用のクライアントを入れなくても最低限のICOSに対するファイル送受信はできるようになると思います(curl, jq, xmllintあたりは必要ですが)。
IBM Cloud上のPower System仮想サーバーを使う場合、sshでのアクセス方法がどうなるのかがよく分かっていませんが、scpでのアクセス速度が制限されているのであれば、サイズの大きなファイル転送はICOS経由の方が早い可能性もあるので、その場合この仕組みが利用できるのではないかと思います。

※ICOS上のディレクトリ作成/削除がイマイチ分からなかった...。とりあえずこれだけあれば足りそうなのと、調べるの面倒だったので一旦ここまで。