もがもがしいブログ

もがもがしく生きる

ESXi 8.0 Nested Installation: disk device does not support OSDATA

ESXi 8.0 (20513097) を Nested でインストールしようとしたところ、以下のエラーとなった。

------ An unexpected error occurred ------
See logs for details

RuntimeError: mpx.vmhba0:C0:T0:L0: disk device does not support OSDATA

RuntimeError: mpx.vmhba0:C0:T0:L0: disk device does not support OSDATA

どうやら ESXi 7.0 では 4 GB で十分だった Boot Media Size が、ESXi 8.0 では 8 GB 以上の要件となったもよう。

* ESXi System Storage Overview (ESXi 7.0)
https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.esxi.install.doc/GUID-474D003B-C6FB-465D-BC1B-5FD30F8E2209.html

Boot Media Size        4-10 GB

* ESXi System Storage Overview (ESXi 8.0)
https://docs.vmware.com/en/VMware-vSphere/8.0/vsphere-esxi-upgrade/GUID-474D003B-C6FB-465D-BC1B-5FD30F8E2209.html

Boot Media Size        8-10 GB

Hard Disk 1 のサイズを 8 GB に拡張して再度試したがダメ。9 GB でもダメ。10 GB にしたらインストールできた。

Select a Disk to Install or Upgrade

ESXi 8.0.0 has been installed successfully.

straceをStatic LinkでBuildする

Prerequisites

弊社が提供しているApplianceは基本的にBlack Boxで、開発/Sustaining/QA以外の人間がソースコードを読むことはできません。 自分のようなサポートエンジニアであれば、特殊なライセンスを用いてShell Mode (bash)に入り、一般的なLinuxのコマンドでトラブルシューティングを行うことができます。

今回は、いくつかあるAppliance製品の中で例外的に "ユーザが特殊ライセンス無しでShell Modeに入れるものの、トラブルシュートに有用なコマンドが用意されていない" 製品にstraceをStatic Buildで入れてみました。

Build Env

CentOS 8.1.1911
Linux kernel 4.18.0-147.5.1.el8_1.x86_64
gcc version 8.3.1 20190507 (Red Hat 8.3.1-4) (GCC)
GNU Make 4.2.1

Steps Performed

初めに失敗例から。以下GitHub Issueを参考に: github.com

$ git clone https://github.com/strace/strace.git
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /usr/bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking whether to enable maintainer-specific portions of Makefiles... no
checking build system type... x86_64-pc-linux-gnu
checking host system type... x86_64-pc-linux-gnu
checking for gcc... gcc
checking whether the C compiler works... no
configure: error: in `/usr/local/src/strace':
configure: error: C compiler cannot create executables
See `config.log' for more details

$ less config.log
...
/usr/bin/ld: cannot find -lpthread
/usr/bin/ld: cannot find -lc
collect2: error: ld returned 1 exit status
...

どうやら libc.a, libpthread.a がなくて怒られているようです。Yum/RPM packageを探してみても見つからないので、それらのBuildから始めました。 sourceware.org

$ mkdir $HOME/src
$ cd $HOME/src
$ git clone git://sourceware.org/git/glibc.git
$ mkdir -p $HOME/build/glibc
$ cd $HOME/build/glibc
$ $HOME/src/glibc/configure --prefix=/usr
$ make
$ mkdir -p $HOME/lib
$ cp $HOME/build/glibc/libc.a $HOME/lib
$ cp $HOME/build/glibc/rt/librt.a $HOME/lib
$ cp $HOME/build/glibc/nptl/libpthread.a $HOME/lib
$ export LDFLAGS="-L $HOME/lib -static -pthread"

librt.a も必要そうだったので入れました。続いてstraceのBuild。

$ mkdir $HOME/src
$ cd $HOME/src
$ git clone https://github.com/strace/strace.git
$ cd $HOME/src/strace
$ ./bootstrap
$ mkdir -p $HOME/build/strace/
$ cd $HOME/build/strace/
$ $HOME/src/strace/configure
$ make
$ file strace
strace: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=ec5651dd26507ec19b32d8793000d29191b70277, with debug_info, not stripped

無事にBuild完了。SFTPでコピーして動くことが確認できました。

# ./strace -fttTyyy -s1024 echo "Hello World"
09:32:56.028234 execve("/bin/echo", ["echo", "Hello World"], 0x7ffc789af918 /* 19 vars */) = 0 <0.000462>
09:32:56.029066 brk(NULL)               = 0x22ac000 <0.000022>
09:32:56.029273 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f40c01bf000 <0.000025>
09:32:56.029411 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) <0.000025>
09:32:56.029589 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3</etc/ld.so.cache> <0.000023>
09:32:56.029748 fstat(3</etc/ld.so.cache>, {st_mode=S_IFREG|0644, st_size=36688, ...}) = 0 <0.000020>
09:32:56.029874 mmap(NULL, 36688, PROT_READ, MAP_PRIVATE, 3</etc/ld.so.cache>, 0) = 0x7f40c01b6000 <0.000021>
09:32:56.029974 close(3</etc/ld.so.cache>) = 0 <0.000015>
09:32:56.030076 open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3</usr/lib64/libc-2.17.so> <0.000024>
09:32:56.030229 read(3</usr/lib64/libc-2.17.so>, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0@\0\0\0\0\0\0\0(c \0\0\0\0\0\0\0\0\0@\08\0\n\0@\0K\0J\0\6\0\0\0\5\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0000\2\0\0\0\0\0\0000\2\0\0\0\0\0\0\10\0\0\0\0\0\0\0\3\0\0\0\4\0\0\0\240I\30\0\0\0\0\0\240I\30\0\0\0\0\0\240I\30\0\0\0\0\0\34\0\0\0\0\0\0\0\34\0\0\0\0\0\0\0\20\0\0\0\0\0\0\0\1\0\0\0\5\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\224}\33\0\0\0\0\0\224}\33\0\0\0\0\0\0\0 \0\0\0\0\0\1\0\0\0\6\0\0\0000\207\33\0\0\0\0\0000\207;\0\0\0\0\0000\207;\0\0\0\0\0pQ\0\0\0\0\0\0\220\232\0\0\0\0\0\0\0\0 \0\0\0\0\0\2\0\0\0\6\0\0\0\200\273\33\0\0\0\0\0\200\273;\0\0\0\0\0\200\273;\0\0\0\0\0\360\1\0\0\0\0\0\0\360\1\0\0\0\0\0\0\10\0\0\0\0\0\0\0\4\0\0\0\4\0\0\0p\2\0\0\0\0\0\0p\2\0\0\0\0\0\0p\2\0\0\0\0\0\0D\0\0\0\0\0\0\0D\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\7\0\0\0\4\0\0\0000\207\33\0\0\0\0\0000\207;\0\0\0\0\0000\207;\0\0\0\0\0\20\0\0\0\0\0\0\0\220\0\0\0\0\0\0\0\20\0\0\0\0\0\0\0P\345td\4\0\0\0\274I\30\0\0\0\0\0\274I\30\0\0\0\0\0\274I\30\0\0\0\0\0004i\0\0\0\0\0\0004i\0\0\0\0\0\0\4\0\0\0\0\0\0\0Q\345td\6\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\20\0\0\0\0\0\0\0R\345td\4\0\0\0000\207\33\0\0\0\0\0000\207;\0\0\0\0\0000\207;\0\0\0\0\0\3208\0\0\0\0\0\0\3208\0\0\0\0\0\0\1\0\0\0\0\0\0\0\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\225\377\2\244\276\272\274W<x'\246mD\177{\253\335\252D\4\0\0\0\20\0\0\0\1\0\0\0GNU\0\0\0\0\0\2\0\0\0\6\0\0\0 \0\0\0\0\0\0\0\363\3\0\0\t\0\0\0\0\1\0\0\16\0\0\0\0000\20D\240 \2\1\210\3\346\220\305E\214\0\300\0\10\0\5\200\0`\300\200\0\r\212\f\0\4\20\0\210D2\10.@\210T<, \0162H&\204\300\214\4\10\0\2\2\16\241\254\32\4f\300\0\3002\0\300\0P\1 \201\10\204\v  ($\0\4 Z\0\20X\200\312DB(\0\6\200\20\30B\0 @\200\0\tP\0Q\212@\20\0\0\0\0\10\0\0\21\20", 832) = 832 <0.000020>
09:32:56.030404 fstat(3</usr/lib64/libc-2.17.so>, {st_mode=S_IFREG|0755, st_size=2127336, ...}) = 0 <0.000019>
09:32:56.030523 mmap(NULL, 3940800, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3</usr/lib64/libc-2.17.so>, 0) = 0x7f40bfbdc000 <0.000024>
09:32:56.030631 mprotect(0x7f40bfd94000, 2097152, PROT_NONE) = 0 <0.000033>
09:32:56.030729 mmap(0x7f40bff94000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3</usr/lib64/libc-2.17.so>, 0x1b8000) = 0x7f40bff94000 <0.000029>
09:32:56.030857 mmap(0x7f40bff9a000, 16832, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f40bff9a000 <0.000021>
09:32:56.030962 close(3</usr/lib64/libc-2.17.so>) = 0 <0.000016>
09:32:56.031075 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f40c01b5000 <0.000018>
09:32:56.031172 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f40c01b3000 <0.000036>
09:32:56.031285 arch_prctl(ARCH_SET_FS, 0x7f40c01b3740) = 0 <0.000016>
09:32:56.031479 mprotect(0x7f40bff94000, 16384, PROT_READ) = 0 <0.000023>
09:32:56.031579 mprotect(0x606000, 4096, PROT_READ) = 0 <0.000021>
09:32:56.031669 mprotect(0x7f40c01c0000, 4096, PROT_READ) = 0 <0.000024>
09:32:56.031756 munmap(0x7f40c01b6000, 36688) = 0 <0.000031>
09:32:56.031990 brk(NULL)               = 0x22ac000 <0.000016>
09:32:56.032059 brk(0x22cd000)          = 0x22cd000 <0.000019>
09:32:56.032135 brk(NULL)               = 0x22cd000 <0.000027>
09:32:56.032232 open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3</usr/lib/locale/locale-archive> <0.000028>
09:32:56.032362 fstat(3</usr/lib/locale/locale-archive>, {st_mode=S_IFREG|0644, st_size=106070960, ...}) = 0 <0.000018>
09:32:56.032471 mmap(NULL, 106070960, PROT_READ, MAP_PRIVATE, 3</usr/lib/locale/locale-archive>, 0) = 0x7f40b96b3000 <0.000024>
09:32:56.032588 close(3</usr/lib/locale/locale-archive>) = 0 <0.000017>
09:32:56.032763 fstat(1</dev/pts/0<char 136:0>>, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0 <0.000017>
09:32:56.032872 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f40c01be000 <0.000022>
09:32:56.032968 write(1</dev/pts/0<char 136:0>>, "Hello World\n", 12Hello World
) = 12 <0.000027>
09:32:56.033091 close(1</dev/pts/0<char 136:0>>) = 0 <0.000017>
09:32:56.033207 munmap(0x7f40c01be000, 4096) = 0 <0.000027>
09:32:56.033308 close(2</dev/pts/0<char 136:0>>) = 0 <0.000017>
09:32:56.033404 exit_group(0)           = ?
09:32:56.033586 +++ exited with 0 +++

CentOS8: Error: Failed to synchronize cache for repo 'AppStream'

Issue

ある日、sudo dnf updateを実行したところ

$ sudo dnf update
CentOS-8.0 - AppStream                                                                                                                                                                                                                          27  B/s |  38  B     00:01
Error: Failed to synchronize cache for repo 'AppStream'

とエラーが出てupdateできず。dnf.logを確認すると

$ sudo less /var/log/dnf.log
...
2020-01-22T00:23:05Z DEBUG Cannot download 'http://mirrorlist.centos.org/?release=8.0&arch=x86_64&repo=AppStream&infra=stock': Cannot prepare internal mirrorlist: No URLs in mirrorlist.
...

http://mirrorlist.centos.org/?release=8.0&arch=x86_64&repo=AppStream へアクセスすると "Invalid release/repo/arch combination" とのこと。

$ python3 -c 'import dnf, pprint; db = dnf.dnf.Base(); pprint.pprint(db.conf.substitutions,width=1)'
{'arch': 'x86_64',
 'basearch': 'x86_64',
 'releasever': '8.0'}

Resolution

どうやらrelease=8.0は存在せずrelease=8でなければならないようで、レポジトリ修正。

$ sudo vi /etc/yum.repos.d/CentOS-AppStream.repo
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=AppStream&infra=$infra
mirrorlist=http://mirrorlist.centos.org/?release=8&arch=$basearch&repo=AppStream&infra=$infra

$ sudo vi /etc/yum.repos.d/CentOS-Base.repo
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=BaseOS&infra=$infra
mirrorlist=http://mirrorlist.centos.org/?release=8&arch=$basearch&repo=BaseOS&infra=$infra

その後sudo dnf updateしたら解決しました。repoのmirrorlistをrelease=$releaseverに戻しても動く。

$ python3 -c 'import dnf, pprint; db = dnf.dnf.Base(); pprint.pprint(db.conf.substitutions,width=1)'
{'arch': 'x86_64',
 'basearch': 'x86_64',
 'releasever': '8'}

そもそもどうしてreleaseverが8.0になったのかは謎。。。

Reference

0016456: CentOS 8 Netinstall broken: "Error setting up base repository" - CentOS Bug Tracker

拡張子のないファイルのデフォルトアプリをNotepad++にする

Chromeで拡張子のないファイル(metadata等)をダウンロードして開こうとしたところ

This file does not have an app associated with it for performing this action. Please install an app or, if one is already installed, create an association in the Default Apps Settings page.

f:id:moga_shi:20191228101106p:plain

のエラーに遭遇。既定のアプリ (Default apps) -> Choose default apps by file type を見ても、"拡張子のないファイルの関連付け" はどうすればいいのか不明な状況。

そこで、regedit.exe を実行しComputer\HKEY_CLASSES_ROOT配下で . の Key を作成。Data はtxtfileを指定。

f:id:moga_shi:20191228101054p:plain f:id:moga_shi:20191228102401p:plain

続いて、Computer\HKEY_CLASSES_ROOT\txtfile\shell\open\commandの Data を%SystemRoot%\system32\NOTEPAD.EXE %1から"D:\Program Files\Notepad++\notepad++.exe" "%1"に変更:

f:id:moga_shi:20191228102410p:plain

このままだとアイコンがデフォルトの Notepad のままなので、Computer\HKEY_CLASSES_ROOT\txtfile\DefaultIconの Data を%SystemRoot%\system32\imageres.dll,-102から"D:\Program Files\Notepad++\notepad++.exe"に変更。

f:id:moga_shi:20191228102417p:plain

できました。

f:id:moga_shi:20191228102538p:plain

なお、ftypeを使えばもっと楽にできるようです。 qiita.com

Amazon Connect+Transcribe+KVS+SNS で留守番電話をテキストに起こしてSMSで通知するシステムを作った

f:id:moga_shi:20191202204443p:plain

みなさん、突発的にかかってくる電話は好きですか?

自分は、"それまでの思考や作業が中断され""同期的なコミュニケーションを強いられる上に""だいたいの場合においてこっちにメリットがない" という3点で大嫌いです

留守電にしても、ヘッダー情報が電話番号くらいしかないメッセージを開くのが手間に思えます。*1


留守番電話をテキストにして通知してくれればいいのに・・・*2

f:id:moga_shi:20191202203502p:plain



というわけでAWS上に置いたシステムがこちらです。

f:id:moga_shi:20191202203608p:plain

コンポーネントを簡単に説明していきます。


(1) Calls +81-50-xxxx-xxxx

まずは電話番号の取得から。Amazon Connect でインスタンスを作成し、出来上がったポータルに admin でログインして申請します。*3

f:id:moga_shi:20191202202847p:plain

f:id:moga_shi:20191202202851p:plain


(2) Invokes Lambda

Contact flow を作成し、Lambda を Invoke できるようにします。

f:id:moga_shi:20191202205206p:plain

途中の Play prompt (録音待ち) の部分は SSML の break time を使用したのですが、10秒以上の待ち時間を指定するとうまく待ってくれない事象が起きたため、2つの要素に分割しました。*4

<speak>
<break time="10s"/><p/>
<break time="10s"/><p/>
承りました<p/>
</speak>

(3) Records audio to KVS (raw data format)

(3’) Stores session information in S3

(4) Converts KVS audio to WAV

こちらに関しては、クラスメソッド平内さんのソースを パク 借用しただけなので割愛させていただきます。Rawデータ変換の部分は本当に助かりました。

dev.classmethod.jp

なお、const ebml = require('ebml'); が "errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'ebml'" でコケたので、こちらを参考に ebml の Layer を追加して対応しました。

xp-cloud.jp

(5) Transcribes text from WAV

WAVから文字起こしの処理です。S3 bucket が input/output で分かれているのは、Amazon Transcribe で S3 の出力先フォルダを指定する方法が見つからなかったため、管理の都合上やむを得ず分割しました。

InvokeTranscription

const AWS = require("aws-sdk");
const region = 'ap-northeast-1';
const inBucket = 'connect-voicemail';
const outBucket = 'connect-voicemail-text';

/**
 * exports.handler
 */
exports.handler = async(event) => {

  const transcribe = new TranscribeService(AWS, region);
  
  for (let record of event.Records) {
    const key = record.s3.object.key;
    const jobName = key.split('/')[1];
    const uri = 'https://' + inBucket + '.s3-' + region + '.amazonaws.com/' + key;
    var res = await transcribe.start(jobName, uri);
    console.log('res: ' + JSON.stringify(res));
  }
  return {};
  
};

/**
 * Class TranscribeService
 */
class TranscribeService {
  
  constructor(AWS, region) {
    this._transcribe = new AWS.TranscribeService({ apiVersion: '2017-10-26', region: region });
  }
  
  async start(jobName, uri) {
    const params = {
      'TranscriptionJobName': jobName,
      'LanguageCode': 'ja-JP',
      'Media': {
        'MediaFileUri': uri
      },
      'MediaFormat': 'wav',
      'MediaSampleRateHertz': 8000,
      'OutputBucketName': outBucket,
    };
    
    return await this._transcribe.startTranscriptionJob(params).promise();
  }
}

(6) Sends SMS to my phone (+81-80-xxxx-xxxx)

SendTranscriptionBySMS

import boto3
import logging
import json

s3 = boto3.client('s3')
sns = boto3.client('sns')
logger = logging.getLogger()

topicArn = 'arn:aws:sns:ap-northeast-1:<Account ID>:TranscriptionSNS'

def handler(event, context):

  # Get transcription from S3 object (JSON)
  bucket = event['Records'][0]['s3']['bucket']['name']
  key = event['Records'][0]['s3']['object']['key']
  s3Object = s3.get_object(
    Bucket = bucket,
    Key = key
  )
  content = json.loads(s3Object['Body'].read().decode('utf-8'))  

  for transcript in content['results']['transcripts']:
    
    logger.warn(transcript)
    
    # TODO Shorten message if the length is larger than 160 bytes
    message = transcript['transcript']
    
    res = sns.publish(
      TopicArn = topicArn,
      Message = message
    )
    #logger.warn(res)

  return {};

SMSのテスト中に "No quota left for account" というエラーに引っ掛かり、調べたところ、デフォルトの Quota が 1 USD / month (about 15 SMS messages per month) となっているとのことだったので サポートケースをオープンして上限を引き上げました。

aws.amazon.com


4時間ほどで返信あり。

Hello,

Your new SMS monthly spending limit of $10 USD was implemented. This may take up to one hour to reflect in your console.

Before you can send messages, you must update your account spend limit using the Amazon PINPOINT console or API ( https://docs.aws.amazon.com/pinpoint/latest/userguide/channels-sms-setup.html ).

When you complete these procedures, you may see a message stating that your default limit is $1.00. You can disregard this message.

We recommend monitoring metrics for Amazon Pinpoint using CloudWatch ( https://docs.aws.amazon.com/pinpoint/latest/userguide/monitoring.html ).

As you get started with Amazon Pinpoint, we recommend that you:
Thank you for choosing Amazon Web Services.

Best regards,


Amazon PINPOINT で Limit を Update せよとのことでしたが、それはせず普通に [Amazon SNS] -> [Text messaging (SMS)] -> [Text messaging preferences] -> [Account spend limit] の設定を変えれば即座に反映されました。

f:id:moga_shi:20191202202900p:plain

ためしに迷惑電話っぽい電話を自分にかけてテストしました。ところどころうまくいってないのはたぶん滑舌のせい。(だから電話はイヤなのだ)

追記:

本記事公開のわずか2日後に発表された Contact Lens for Amazon Connect (Preview) に Sign up すると、音声の文字起こし部分を AWS-Managed で利用できるようです。
dev.classmethod.jp
aws.amazon.com

*1:イヤなものは全部イヤに見えてくるのだ

*2:キャリアSIMだとあるらしいですね

*3:日本の電話番号は 050 のみ。1 インスタンスあたり 10 番号まで申請できます。 https://docs.aws.amazon.com/connect/latest/adminguide/connect-tokyo-region.html https://docs.aws.amazon.com/connect/latest/adminguide/amazon-connect-service-limits.html

*4:今の実装だと、留守電を途中(「承りました」の前)で切られた際にその後の Invoke が効かなくなるという不具合があり・・・ break time を短くする以外の Workaround があるといいのですが。

VCSA 6.7 U2: "Error message: There was no valid template paths for execution, please double check the given path"

vcsa-deploy が以下エラーでコケる件。

# /usr/local/src/vcsa/vcsa-cli-installer/lin64/vcsa-deploy install --accept-eula --acknowledge-ceip -v --no-ssl-certificate-verification --verify-template-only template /usr/local/src/vcsa/vcsa-cli-installer/templates/install/embedded_vCSA_on_ESXi.json
...
Only either the path to a template, or to a directory of templates is allowed.
Error message: There was no valid template paths for execution, please double check the given path ['template', '/root/vCSA_on_ESXi.json']
================================================================================================================================== 00:05:18 ==================================================================================================================================
Result and Log File Information...
WorkFlow log directory: /tmp/vcsaCliInstaller-2019-07-23-00-05-ikyhfryi/workflow_1563840318311

パスは通ってるし文法も間違ってないのになぜ…と思ったら、"template" の文字列が不要でした。

i.e.

# /usr/local/src/vcsa/vcsa-cli-installer/lin64/vcsa-deploy 
install --accept-eula --acknowledge-ceip -v --no-ssl-certificate-verification /usr/local/src/vcsa/vcsa-cli-installer/templates/install/embedded_vCSA_on_ESXi.json


helpには template の記載があるトラップ。。。

# /usr/local/src/vcsa/vcsa-cli-installer/lin64/vcsa-deploy install --help
Usage: vcsa-deploy install [-h] [--template-help] [--log-dir LOG_DIR] [--skip-ovftool-verification] [--accept-eula] [--acknowledge-ceip] [--pause-on-warnings] [--operation-id OPERATION_ID] [-v | -t] [--no-esx-ssl-verify | --no-ssl-certificate-verification]
                           [--verify-template-only | --precheck-only]
                           template [template ...]

Deploy VCSA to a remote host.

Tropo API を利用した自動音声応答装置(IVR)の構築サンプル

あけましておめでとうございます。@moga_shiです。
去年は初配属の年で、あまり業務外のことに時間を割けなかったのですが、最近社内失職気味に
なってきたので、やる気さえあればいろいろ活動できそうな感じです。やる気さえあれば。
というわけで新年最初の記事です。クラウド電話APITropo』をご紹介します。

はじめに

クラウド電話APIといえば『Twilio』を思い浮かべる人は多いのではないでしょうか。
率直に言って、国内でクラウド電話APIを利用したサービスを開発したいのであれば、
日本語ドキュメントやサポートが充実している Twilio を利用するのが現状では最適解だと思います。
ただ、Twilio を検証目的で利用する場合、以下の点で少し困ります。

  • 月額基本料金490円 + 従量料金がかかる
  • 無料のトライアルアカウントだと冒頭でデモ用メッセージが流れる

まぁ、月々にわずかな金額を支払えば済む話ではあるのですが、検証用に複数の電話番号を使いたい時や、
お遊びで一瞬だけ公開するアプリを作りたい時には少々困った話になります。
そんな時に役に立つのが Tropo です。
トライアルの Development Use に限り、基本料金や発着信の料金がかかりません。*1*2
電話番号は、東京の03番号を始めとする数十カ国の番号を5つ程度まで持つことができます。*3


使い方

開発するサービスへの接続方法により、Scripting API と Web API の2通りの使い方があります。
前者がホスティング、後者が自前のサーバを使う方法ですが、図で描くと以下のようになります。

  • Scripting API

f:id:moga_shi:20140106231841p:plain

f:id:moga_shi:20140106231848p:plain
データベースや他サービスと連携したい場合は Web API の方を使うことが多くなると思いますので
以下のサンプルでは Web API を利用した事例で説明します。

サンプル

運送会社の再配達受付サービスを考えます。
荷物の配達時に家の人が不在だった場合、不在票を入れておき、後日そこに記載されている番号に
電話をかけると、伝票番号や再配達日時を入力するように求められ・・・というおなじみのシステムです。

サンプルコードは PHP ですが、他に Ruby, Node.js, Python が使えます。
コードがアレなのですが、いい書き方があればご教示ください。

[redeliver/config.php]

<?php
$DSN = array(
        'phptype' => 'mysql',
        'username' => 'YOUR_USERNAME',
        'password' => 'YOUR_PASSWORD',
        'hostspec' => 'localhost',
        'database' => 'YOUR_DATABASE',
);
$BASE_URL = 'http://[YOUR_SERVER]/redeliver/';
$VOICE_URL = $BASE_URL . 'voice/';    // 音声ディレクトリ(音声は CeVIO Creative Studio で作成)
?>


[redeliver/index.php]

<?php
require_once 'MDB2.php';
require_once 'tropo.class.php';    // https://github.com/tropo/tropo-webapi-php
require_once 'lib/limonade.php';    // http://limonade-php.github.io/

/**
 * 初回アクセス
 */
dispatch_post('/welcome', 'app_welcome');
function app_welcome() {

        require_once 'config.php';

        $tropo = new Tropo();
        $session = new Session();

        $from = $session->getFrom();
        $callerID = $from["id"];

        ### Connect to Database ###
        $mdb2 =& MDB2::connect($DSN);
        if (PEAR::isError($mdb2)) {
                die($mdb2->getMessage());
        }

        ### Access History ###
        $res =& $mdb2->exec("INSERT INTO history (tel_no) VALUES ('$callerID')");
        if (PEAR::isError($res)) {
                die($mdb2->getMessage());
        }

        $tropo->say($VOICE_URL . "welcome.mp3");    // "こちらは再配達受付センターです。これから流れるメッセージに従って入力してください。"
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_register"));

        $tropo->RenderJson();
}

/**
 * 伝票番号入力
 */
dispatch_post('/slip_register', 'app_slip_register');
function app_slip_register() {

        require_once 'config.php';

        $tropo = new Tropo();

        $options = array("choices" => "[12 DIGITS]", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->ask($VOICE_URL . "slip_register.mp3", $options);    // "伝票番号12桁を入力してください。" ※0~9の12桁の数字なら何でも OK にしてます。
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_confirm"));

        $tropo->RenderJson();
}

/**
 * 伝票番号確認
 */
dispatch_post('/slip_confirm', 'app_slip_confirm');
function app_slip_confirm() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        $slip_no = $result->getValue();
        $options = array("choices" => "1,3", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->say($VOICE_URL . "slip_confirm.mp3");    // "伝票番号は"
        for ($i=0; $i<12; $i++) {
                $tropo->say($VOICE_URL . "0" . substr($slip_no, $i, 1) . ".mp3");
        }
        $tropo->ask($VOICE_URL . "confirm.mp3", $options);      // "ですね? よろしければ 1 を、訂正する場合は 3 を入力してください。"
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_search&slip_no=$slip_no"));

        $tropo->RenderJson();
}

/**
 * 伝票番号検索(未実装)
 */
dispatch_post('/slip_search', 'app_slip_search');
function app_slip_search() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        $choice = $result->getValue();
        $slip_no = $_GET['slip_no'];

        switch ($choice) {
                case "1":
                        /**
                         * 本当ならここで伝票番号が正しいか判定する
                         */
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
                case "3":
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_register"));
                        break;
                default:
                        $tropo->say($VOICE_URL . "wrong.mp3");    // "入力に誤りがあります。"
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_register"));
                        break;
        }

        $tropo->RenderJson();
}

/**
 * 再配達日入力
 */
dispatch_post('/date_register', 'app_date_register');
function app_date_register() {

        require_once 'config.php';

        $tropo = new Tropo();

        $slip_no = $_GET['slip_no'];

        $options = array("choices" => "[4 DIGITS]", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->ask($VOICE_URL . "date_register.mp3", $options);    // "再配達の希望日を4桁で入力してください。たとえば、1月23日なら 0123 と入力してください。"
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_confirm&slip_no=$slip_no"));

        $tropo->RenderJson();
}

/**
 * 再配達日確認
 */
dispatch_post('/date_confirm', 'app_date_confirm');
function app_date_confirm() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        // 日付チェックは省略...
        $date = $result->getValue();
        $month = substr("$date", 0, 2);
        $day = substr("$date", 2, 2);
        $slip_no = $_GET['slip_no'];
        $options = array("choices" => "1,3", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->say($VOICE_URL . "date_confirm.mp3");    // "ご希望の配達日は"
        $tropo->say($VOICE_URL . $month . ".mp3");
        $tropo->say($VOICE_URL . "gatsu.mp3");    // "月"
        $tropo->say($VOICE_URL . $day . ".mp3");
        $tropo->say($VOICE_URL . "nichi.mp3");    // "日"
        $tropo->ask($VOICE_URL . "confirm.mp3", $options);    // "ですね? よろしければ 1 を、訂正する場合は 3 を入力してください。"
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=time_register&slip_no=$slip_no&date=$date"));

        $tropo->RenderJson();
}

/**
 * 再配達時間帯入力
 */
dispatch_post('/time_register', 'app_time_register');
function app_time_register() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        $choice = $result->getValue();
        $slip_no = $_GET['slip_no'];
        $date = $_GET['date'];
        $options = array("choices" => "0,1,2,3,4,5,6", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        switch ($choice) {
                case "1":
                        $tropo->say($VOICE_URL . "time_register01.mp3");    // "再配達の時間帯を入力してください。"
                        $tropo->ask($VOICE_URL . "time_register02.mp3", $options);    // "時間指定がない場合は 0、午前中は 1、12時から14時は 2、14時から16時は 3、16時から18時は 4、18時から20時は 5、20時から21時は 6 を入力してください。"
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=time_confirm&slip_no=$slip_no&date=$date"));
                        break;
                case "3":
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
                default:
                        $tropo->say($VOICE_URL . "wrong.mp3");    // "入力に誤りがあります。"
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
        }

        $tropo->RenderJson();
}

/**
 * 再配達時間帯確認
 */
dispatch_post('/time_confirm', 'app_time_confirm');
function app_time_confirm() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        // 時間帯チェックは省略...
        $time = $result->getValue();
        $slip_no = $_GET['slip_no'];
        $date = $_GET['date'];
        $month = substr("$date", 0, 2);
        $day = substr("$date", 2, 2);
        $options = array("choices" => "1,3", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->say($VOICE_URL . "time_confirm.mp3");    // "ご希望の配達日時は"
        $tropo->say($VOICE_URL . $month . ".mp3");
        $tropo->say($VOICE_URL . "gatsu.mp3");    // "月"
        $tropo->say($VOICE_URL . $day . ".mp3");
        $tropo->say($VOICE_URL . "nichi.mp3");    // "日"
        $tropo->say($VOICE_URL . "time0" . $time . ".mp3");
        $tropo->ask($VOICE_URL . "confirm.mp3", $options);    // "ですね? よろしければ 1 を、訂正する場合は 3 を入力してください。"

        $tropo->on(array("event" => "continue", "next" => "index.php?uri=complete&slip_no=$slip_no&date=$date&time=$time"));

        $tropo->RenderJson();
}

/**
 * 受付完了
 */
dispatch_post('/complete', 'app_complete');
function app_complete() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        $choice = $result->getValue();
        $slip_no = $_GET['slip_no'];
        $date = $_GET['date'];
        $time = $_GET['time'];

        switch ($choice) {
                case "1":
                        /**
                         * 本当ならここでデータベースを更新する
                         */
                        $tropo->say($VOICE_URL . "complete.mp3");    // "再配達のご依頼を承りました。ありがとうございました。"
                        break;

                case "3":
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
                default:
                        $tropo->say($VOICE_URL . "wrong.mp3");    // "入力に誤りがあります。"
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
        }

        $tropo->RenderJson();
}

run();
?>


以上のサンプルコードを実装した電話番号が 03-4578-2345 です。よろしければお試しください。

*1:もっとも、電話回線の料金は Tropo が負担しているため、検証が終わり次第 Production Account へ移行するか番号をリリースするのが筋なのですが。

*2:アウトバウンドコールにはアカウントの認証が必要です。https://www.tropo.com/docs/webapi/quickstarts/making-call

*3:今のところ明確な上限はないようです。9つまで電話番号を増やしたところ、検証目的を説明するか番号をリリースしてくださいとの連絡を受けました。