Cisco UCSのCIMCにssh接続して設定を行う


Cisco UCSのCisco Integrated Management Controller (CIMC)はWeb管理画面とssh接続によるCLI管理がある。

CIMCについているホスト名は基本的に「サーバ機種名-シリアル番号」となっている。

ホスト名を確認する操作をCLIから行うには、scope:cimc/network以下でdetailを表示する操作になる。

ホスト名# scope cimc/network
ホスト名 /cimc/network # show detail
Network Setting:
    IPv4 Enabled: yes
    IPv4 Address: xxx.xxx.xxx.xxx
    IPv4 Netmask: 255.255.255.0
    IPv4 Gateway: xxx.xxx.xxx.xxx
    DHCP Enabled: no
    DDNS Enabled: yes
    DDNS Update Domain:
    DDNS Refresh Interval(0-8736 Hr): 0
    Obtain DNS Server by DHCP: no
    Preferred DNS: xxx.xxx.xxx.xxx
    Alternate DNS: 0.0.0.0
    IPv6 Enabled: yes
    IPv6 Address: ::
    IPv6 Prefix: 64
    IPv6 Gateway: ::
    IPv6 Link Local: fe80::86b8:xxxx:xxxx:xxxx
    IPv6 SLAAC Address: ::
    IPV6 DHCP Enabled: yes
    IPV6 Obtain DNS Server by DHCP: yes
    IPV6 Preferred DNS: ::
    IPV6 Alternate DNS: ::
    VLAN Enabled: no
    VLAN ID: 1
    VLAN Priority: 0
    Port Profile:
    Hostname: ホスト名
    MAC Address: XX:XX:XX:XX:XX:XX
    NIC Mode: dedicated
    NIC Redundancy: none
    VIC Slot: riser1
    Auto Negotiate: yes
    Admin Network Speed: auto
    Admin Duplex: auto
    Operational Network Speed: 1Gbps
    Operational Duplex: full
ホスト名 /cimc/network #

show detailだといろんな項目が表示されすぎるので、ホスト名だけを取り出すことができないか調べたところ「| grep キーワード」が使えた。

ホスト名 /cimc/network # show detail | grep Hostname
    Hostname: ホスト名
ホスト名 /cimc/network #

ホスト名変更操作は scope:cimc/network にて set hostnameを実行したあと、commitで確定する。

ホスト名 /cimc/network # set hostname 新ホスト名
Create new certificate with CN as new hostname? [y|N] y

ホスト名 /cimc/network *# commit
Changes to the network settings will be applied immediately.
You may lose connectivity to the Cisco IMC and may have to log in again.
Do you wish to continue? [y/N] y

注意点として、ホスト名変更に伴い、Web管理GUIおよびssh接続で使用するSSL証明書で使用するCN(common name)が変更されるため証明書が作成されるということがある。

また、再作成に伴いCIMC自体も再起動されるため、commit後、再起動完了までの数分間CIMCに接続できなくなる。

このため、CIMCホスト名変更処理を行ったあとは、再起動待ちと証明書再発行にともなうssh接続時のキー変更に対応する処理を入れる必要がある。

ssh接続時のknown_hostsファイルから該当するエントリを削除したい場合は、 ssh-keygenコマンドの-Rオプションを使うことで行える。

osakanataro@ubuntu2004:~/imc$ ssh-keygen -R xxx.xxx.xxx.xxx
# Host xxx.xxx.xxx.xxx found: line 1
/home/osakanataro/.ssh/known_hosts updated.
Original contents retained as /home/osakanataro/.ssh/known_hosts.old
osakanataro@ubuntu2004:~/imc$

これで材料が揃ったので、スクリプトを作成する。

最初は Ciscoのcimc-ansible , cimcsdk を使用できないか検討したのですが、どちらもCIMCのホスト名変更に関する処理が実装されていないようだったので、expectコマンドによる処理を採用しました。

作成するにあたり下記を参考にしています。
How to programmatically enable redfish on Cisco CIMC?
Automate the UCS CLI with expect

今回作成したスクリプトは下記の様になりました。

#!/usr/bin/expect -f

# CIMCへの接続に時間がかかるようで
# 標準設定のtimeout値だとコマンド実行前にプロセスが進んでしまう 
# -1 を設定すると応答があるまで待つが
# ホスト名変更処理後は再起動がかかり、-1だと再起動が終わるまで待つことになってしまい時間がかかるので20に設定
set timeout 20

set CIMCaddr "xxx.xxx.xxx.xxx"
set CIMCuser "admin"
set CIMCpass "パスワード"
set CIMChostname "新ホスト名"


spawn ssh -l $CIMCuser -t $CIMCaddr
expect_after eof {exit 1}
# ログイン処理
expect {
        "*?assword:*" {
                send -- "$CIMCpass\r"
        }
        "(yes/no*)*" {
                send -- "yes\r"
                expect "*?assword:*"
                send -- "$CIMCpass\r"
        }
	# ホスト名変更時にssl証明書再作成が行われるため
	# キーが変わることに対する対応
	# リトライ処理が面倒だったので、古いキーを削除するところまでしか行わない
	# 必要に応じて手動で再実行で対応
        "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED" {
                system ssh-keygen -R $CIMCaddr
                puts "\nplease re-exec this script\n"
                exit 2
        }
}

expect "# "
send -- "scope cimc/network\r"
expect "# "

# 現在のホスト名確認
send -- "show detail | grep Hostname \r"
expect -indices -re "Hostname: (.*)\r"
# 文字列検出に使った": "の2文字分を足す
set strst [string last ": " $expect_out(buffer)]
set strst [expr $strst + 2]
# 行の終わりの改行分を引く
set stred [string length $expect_out(buffer)]
set stred [expr $stred - 2]
set hostnamenow [string range $expect_out(buffer) $strst $stred]
puts "CIMChostname: $CIMChostname"
puts "hostname: $hostnamenow"

# ホスト名の変更が必要か?
if { "$CIMChostname" != "$hostnamenow" } {
        puts "change hostname"
} else {
        puts "no change"
        expect "# "
        send -- "top\r"
        expect "# "
        send -- "exit\r"
        exit 0
}

# ホスト名変更
expect "# "
send -- "set hostname $CIMChostname\r"
expect "*new hostname?*"
send -- "y\r"
expect "# "
send -- "show detail | grep Hostname \r"
expect "# "
send -- "commit\r"
expect "*\[y/N] "
send -- "y\r"

# ホスト名確認
#  ただし実際には再起動が掛かっているので実行できない
expect "# "
send -- "show detail | grep Hostname \r"

expect "# "
send -- "top\r"
expect "# "
send -- "exit\r"

exit 0

expectでスクリプトを作った時のメモ書き


ssh経由の操作をexpectコマンドで自動化しようとした時に、expectで調べると、入出力関連に関しては出てくるが、文字列操作や制御構文周りがよくわからない。

ここら辺は、tclに関して調べるとわかるようになっている。

参考にしたサイト

LInux JM Home Page「expect (1)
FreeSoftNet 「Tcl>文法とコマンド
アプリコット PukiWiki「expectで自動化

if文の書き方

他の言語と同じように「if (条件){実行内容}」と書くと妙なエラーになる。

expectでは「if {条件} {実行内容}」というように、どちらも「{}」で囲む。
また、「if」と「{」の間、「条件の}」と「実行内容の{」の間のそれぞれにスペースを挟む必要がある。

文字列を比較して、異なる場合は「diff」、同じであれば「same」と出力するexpectは下記の様になる。

#!/usr/bin/expect -f
set hostnamenew "testhostnew"
set hostnamenow "testhost"

puts "hostnamenew: $hostnamenew"
puts "hostnamenow: $hostnamenow"

if { "$hostnamenew" != "$hostnamenow" } {
        puts "diff"
} else {
        puts "same"
}

誤って「if (条件) {実行内容}」とした場合、下記の様な「unbalanced open paren in expression」というエラーとなる。

unbalanced open paren
in expression "("
    (parsing expression "(")
    invoked from within
"if ( "$hostnamenew" != "$hostnamenow" ) {
        puts "diff"
} else {
        puts "same"
}"

「if(条件) {実行内容}」と「if{条件} {実行内容}」とifと{の間にスペースを入れない場合は下記のような「invalid command name」となる

invalid command name "if("
    while executing
"if( "$hostnamenew" != "$hostnamenow" ){"
invalid command name "if{"
    while executing
"if{ "$hostnamenew" != "$hostnamenow" }{"

面倒くさいことに「if {条件}{実行内容}」と、ifと条件の間にはスペース入れたけど、条件と実行内容の間にスペースがない場合は下記の「extra characters after close-brace while executing」というエラーになる。

extra characters after close-brace
    while executing
"if { "$hostnamenew" != "$hostnamenow" }{"

コマンド出力から文字列を取り出しと文字列の切り出し

expectを使う場合は、sshで他のホストに接続してコマンドを実行する、という用途で使うことが多い。

コマンドの実行結果によって、処理を変えたい場合、どうすればいいのか?

「expect -indices -re “条件式”」で条件式に該当した行を$expect_out(数字,string)で拾うみたいなんだけど、いまいち動作がよくわからない。

「ip a s ens160」を実行して、そのIPアドレスを取得したい場合の例として以下を作った

expect "\[#$] "
send -- "ip a s ens160\r"
expect -indices -re "inet (.*)\r"
puts "===buffer==="
puts "$expect_out(buffer)"
puts "===str0==="
puts "$expect_out(0,string)"
puts "===str1==="
puts "$expect_out(1,string)"
puts "===="

これの実行結果は下記となった。

2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:33:27:f8 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
===buffer===
ip a s ens160
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:33:27:f8 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
===str0===
inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
===str1===
172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
====
       valid_lft forever preferred_lft forever
    inet6 fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

条件を「inet (.*)\r」から「inet6 (.*)\r」に変更

2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:33:27:f8 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
===buffer===
ip a s ens160
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:33:27:f8 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
===str0===
inet6 fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
===str1===
fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
====

ここからさらにIPアドレスの部分を取り出すには「string first ~」「string last ~」と「string range ~」を使って文字列を切り出す。

set str $expect_out(1,string)
set stred [string first "/" $str]
puts "[string range $str 0 $stred]"
set stred [expr $stred - 1]
puts "[string range $str 0 $stred]"

上記の実行結果としては下記のようになる。

172.17.44.48/
172.17.44.48

他の言語のsubstring系だと開始アドレスと、そこを起点に取り出す文字列の長さを指定するが、tclのstring range では、開始アドレスと終了アドレスの2つを指定する形になるので注意が必要になる。

リストファイルを順に処理する

サーバ名 IPアドレス ユーザ名 パスワードがかかれている一覧ファイルを読み込ませて順に処理させる例

まず、一覧ファイルの例

server1       172.17.44.51    admin   password123#
server2       172.17.44.52    admin   password123#

スクリプト

#!/usr/bin/expect -f

if { $argc < 1 } {
        puts "Usage: $argv0 <serverlist>"
        exit 1
}

set filename [lindex $argv 0]

set contents [read [open $filename]]
set contentline [split $contents "\n"]
foreach line $contentline {
        #puts "$line"
        set linesplit [split $line "\t"]
        set hostname [lindex $linesplit 0]
        set ipaddr [lindex $linesplit 1]
        set username [lindex $linesplit 2]
        set password [lindex $linesplit 3]
        if { $hostname != "" } {
                puts "hostname:$hostname, ipaddr:$ipaddr, username:$username, password:$password"
        }
}

実行例

$ ./sample3 serverlist
hostname:server1, ipaddr:172.17.44.51, username:admin, password:password123#
hostname:server2, ipaddr:172.17.44.52, username:admin, password:password123#
$

なお、処理スクリプトに「if { $hostname != “” } {」を入れているのは改行のみやEOFのみの行を処理してしまわないようにするため

とはいえ、これを応用してssh接続させるスクリプトにしたところ、間にアクセスできないホストがあると、そこでエラー終了してしまうので、取り扱いが面倒であることが判明。
(エラー時の処理を実装すればいいんだけど、面倒)

よって、bashスクリプト側でリストは処理し、そこからexpectスクリプトを呼び出すこととなった。

CentOS7でrshが使えない


古い環境から移行するために、CentOS7環境でのテストを実施中。

該当環境ではrshを使っていたので、CentOS7にrshパッケージを追加して、コマンドを実行してみると・・・

# rsh 192.168.100.101 hostname
poll: protocol failure in circuit setup
#
# firewall-cmd --get-services
RH-Satellite-6 amanda-client amanda-k5-client amqp amqps apcupsd audit bacula bacula-client bgp bitcoin bitcoin-rpc bitcoin-testnet bitcoin-testnet-rpc ceph ceph-mon cfengine condor-collector ctdb dhcp dhcpv6 dhcpv6-client distcc dns docker-registry docker-swarm dropbox-lansync elasticsearch etcd-client etcd-server finger freeipa-ldap freeipa-ldaps freeipa-replication freeipa-trust ftp ganglia-client ganglia-master git gre high-availability http https imap imaps ipp ipp-client ipsec irc ircs iscsi-target isns jenkins kadmin kerberos kibana klogin kpasswd kprop kshell ldap ldaps libvirt libvirt-tls lightning-network llmnr managesieve matrix mdns minidlna mongodb mosh mountd mqtt mqtt-tls ms-wbt mssql murmur mysql nfs nfs3 nmea-0183 nrpe ntp nut openvpn ovirt-imageio ovirt-storageconsole ovirt-vmconsole plex pmcd pmproxy pmwebapi pmwebapis pop3 pop3s postgresql privoxy proxy-dhcp ptp pulseaudio puppetmaster quassel radius redis rpc-bind rsh rsyncd rtsp salt-master samba samba-client samba-dc sane sip sips slp smtp smtp-submission smtps snmp snmptrap spideroak-lansync squid ssh steam-streaming svdrp svn syncthing syncthing-gui synergy syslog syslog-tls telnet tftp tftp-client tinc tor-socks transmission-client upnp-client vdsm vnc-server wbem-http wbem-https wsman wsmans xdmcp xmpp-bosh xmpp-client xmpp-local xmpp-server zabbix-agent zabbix-server
#

「rsh」というのがあるが、これはサーバ側としての設定で、実際に設定しても状況は変わらない。

# firewall-cmd --permanent --zone=public --add-service=rsh
success
# firewall-cmd --reload
success
# rsh 192.168.100.101 hostname
poll: protocol failure in circuit setup
#

じゃぁ、ポートの何番を開けたらいいのかというあたりについて調べるとRedHat KB「rsh 接続が使用するポート数を確認する」が見つかる。

詳細はログインしないと見れないが、ログインしなくてもみれる部分に「しかし、ファイアウォールでその他のポート (512~1023) も許可しないと、接続が成功しません。」と書いてある。

基本的には1023番から順に使われていないポートを探していく、という設定になっている。

このため通常の実用上は1020~1023の4ポートをあけておけばなんとかなるようである。

なので、firewalldに対する設定としては「firewall-cmd –permanent –zone=public –add-port=1020-1023/tcp」で行うこととする。

# firewall-cmd --permanent --zone=public --add-port=1020-1023/tcp
success
# firewall-cmd --reload
success
# rsh 192.168.100.101 hostname
testserver
#

問題無く動作した。

rsyncによるディレクトリ同期を行う際、並列実行により高速化する手法


rsyncを高速化するために、分散して実行することにした。

全部を1つのスクリプトとしてもいいのだが、デバグがしやすいように分割して作業を行えるようにしている。

また、下記の記述はLinuxの/usrをコピーすることを想定している。環境に応じて書き換えること。

まず、/usr/xxx以下にあるファイルまでをコピーするために以下を実行する。

# rsync --archive -v --exclude="*/*/" /usr/ /mnt/vol/voltest

このexcludeオプションをつけている場合、「/usr/xxx/yyy」のファイルとシンボリックリンクはコピーされる。しかし「/usr/xxx/zzz/」のディレクトリはコピーされない。

次に、「/usr/xxx/zzz/」のディレクトリ一覧を取得する。

# find /usr -mindepth 2 -maxdepth 2 -type d -print

このディレクトリ一覧を下記のperlスクリプトに食わせる。(下記スクリプトは” find /usr -mindepth 2 -maxdepth 2 -type d -print > list.txt”で取得したlist.txtを使う想定)

#!/usr/bin/perl

use threads;
use Thread::Queue;

my $LOGDIR="/root/test";
my $MAXSESSION=5;

my $sourcepathbase="/usr";
my $destpathbase="/mnt/vol/voltest";

my $stream = Thread::Queue->new;

open(FILE,"list.txt");
while(my $tmp=<FILE>){
        $stream->enqueue("$tmp");

}
close(FILE);

sub SyncExecute{
        while(my $str = $stream->dequeue){
                # 改行削除
                $str =~ s/\n//ig;
                $str =~ s/\r//ig;
                # ログ出力用ファイル名
                my $filename=$str;
                $filename =~ s/\//-/ig;
                $filename =~ s/\.//ig;
                $filename =~ s/-$//ig;
                $filename =~ s/#//ig;
                $filename =~ s/^-//ig;
                $filename =~ s/ //ig;
                my $logfile="$LOGDIR/test-$filename.log";
                # rsync元と先の処理
                my $tmp,$st,$ed;
                my $sourcepath,$destpath;
                $tmp=substr($str,0,1);
                if($tmp eq "/"){
                        $sourcepath=$sourcepathbase.$str."/";
                        $destpath=$destpathbase.$str;
                }else{
                        $sourcepath=$sourcepathbase."/".$str."/";
                        $destpath=$destpathbase."/".$str;
                }
                `date >> $logfile`;
                print "rsync -v --archive $sourcepath $destpath >> $logfile 2>&1 \n";
                `rsync -v --archive $sourcepath $destpath >> $logfile 2>&1 `;
                `date >> $logfile`;
                #`sleep 5`;
        }
}


my @kids;
foreach(1..$MAXSESSION){
        my $kid = threads->new(\&SyncExecute,$stream);
        push(@kids,$kid);
        $stream->enqueue(undef);
}


print "wait\n";

foreach(@kids){
        my ($return) =$_ -> join;
}

このスクリプトは、rsyncの同時実行数5で、並列にrsyncを実行していくものになっている。

実行したサーバの負荷状況に応じて「my $MAXSESSION=5;」で設定している 同時実行数 を調整する。あまり大きくしすぎるとサーバからの応答が遅くなりすぎるのでほどほどに・・・


2020/03/10追記

上記で実行するrsyncコマンドはハードリンクの処理を行わないものとなっている。

このため、ハードリンクされているファイルがある場合、コピー先のファイルが1つではなく複数別個のものとしてコピーされる。

ハードリンクをそのままコピーしたい場合は「–hard-links」オプションを追加する必要があるのだが、ハードリンク処理の効力範囲は同一プロセス内で処理すること、という条件があるため、今回のような分割処理して高速化する、という場合には不適切となっている。

このため、ハードリンクファイルがある場合は、初回同期は分割処理で行い、2回目はディレクトリ全体を–hard-linksオプションをつけて1プロセスで処理してハードリンク処理を行わせる、という手法をとる必要がある。

なお、ハードリンク処理が完了したあと、分割処理の対象となった場合、すでにファイルが存在しているので再コピーされる、ということは発生しない。

Raspberry pi上のOSMCにstreamlinkをインストールする


streamlink」をOSMC環境に入れてみた

streamlinkからVLCを起動してHDMI経由の音声出力はできたのですが、HDMI映像出力がうまくいきませんでした。このため、VLCで受信したものをrtspサーバとして出力し、そのデータをkodi側で受け取るという実装にしました。

追加したもの

再生用にVLC

$ sudo apt install vlc vlc-plugin-sdl 

pythonでpipコマンドを使うために

$ sudo apt install python-pip python-setuptools

コンパイルをするために

$ sudo apt install build-essential python-dev

エラー解決のために

下記のエラー解決のために「sudo apt install libffi-dev」

    c/_cffi_backend.c:15:17: fatal error: ffi.h: No such file or directory
     #include <ffi.h>

「sudo apt install libssl-dev」

    build/temp.linux-armv7l-2.7/_openssl.c:498:30: fatal error: openssl/opensslv.h: No such file or directory
     #include <openssl/opensslv.h>

で・・・これでようやくstreamlinkインストールに成功しました。

「sudo pip install streamlink」

再生に至るまで

以下のような感じで、OSMCにログインした状態でコマンドを実行して、ポート8554にてrtspのストリーミングサーバを実行します。

$ streamlink https://www.showroom-live.com/ringo-005 best --player="cvlc --sout '#rtp{sdp=rtsp://:8554/}'"

ちなみに、streamlinkのマニュアルを見ると「–player-args」というオプションで引数を渡すことができるとありましたが、「streamlink https://www.showroom-live.com/ringo-005 best –player=cvlc –player-args=”–sout ‘#rtp{sdp=rtsp://:8554/}'”」を実行すると、以下のエラーになってしまって起動できませんでした。

osmc@osmc:~$ streamlink https://www.showroom-live.com/yui-010 worst --player=cvlc --player-args="--sout '#rtp{sdp=rtsp://:8554/}'"
/usr/local/lib/python2.7/dist-packages/requests/__init__.py:91: RequestsDependencyWarning: urllib3 (1.25.2) or chardet (3.0.4) doesn't match a supported version!
  RequestsDependencyWarning)
[cli][info] Found matching plugin showroom for URL https://www.showroom-live.com/yui-010
[cli][info] Available streams: 144p (worst), low, high, 1080p (best)
[cli][info] Opening stream: 144p (hls)
[cli][info] Starting player: cvlc
[cli][info] Closing currently open stream...
Traceback (most recent call last):
  File "/usr/local/bin/streamlink", line 11, in <module>
    sys.exit(main())
  File "/usr/local/lib/python2.7/dist-packages/streamlink_cli/main.py", line 1033, in main
    handle_url()
  File "/usr/local/lib/python2.7/dist-packages/streamlink_cli/main.py", line 594, in handle_url
    handle_stream(plugin, streams, stream_name)
  File "/usr/local/lib/python2.7/dist-packages/streamlink_cli/main.py", line 447, in handle_stream
    success = output_stream(plugin, stream)
  File "/usr/local/lib/python2.7/dist-packages/streamlink_cli/main.py", line 320, in output_stream
    output.open()
  File "/usr/local/lib/python2.7/dist-packages/streamlink_cli/output.py", line 24, in open
    self._open()
  File "/usr/local/lib/python2.7/dist-packages/streamlink_cli/output.py", line 221, in _open
    self._open_subprocess()
  File "/usr/local/lib/python2.7/dist-packages/streamlink_cli/output.py", line 242, in _open_subprocess
    args = self._create_arguments()
  File "/usr/local/lib/python2.7/dist-packages/streamlink_cli/output.py", line 203, in _create_arguments
    args = self.args.format(filename=filename)
KeyError: 'sdp=rtsp'
osmc@osmc:~$ streamlink https://www.showroom-live.com/yui-010 best --player="cvlc --sout '#rtp{sdp=rtsp://:8554/}'"
/usr/local/lib/python2.7/dist-packages/requests/__init__.py:91: RequestsDependencyWarning: urllib3 (1.25.2) or chardet (3.0.4) doesn't match a supported version!
  RequestsDependencyWarning)
[cli][info] Found matching plugin showroom for URL https://www.showroom-live.com/yui-010
[cli][info] Available streams: 144p (worst), low, high, 1080p (best)
[cli][info] Opening stream: 1080p (hls)
^CInterrupted! Exiting...
[cli][info] Closing currently open stream...
osmc@osmc:~$

続いて、拡張子が.m3uで、以下の内容をテキストエディタで作成します。

#EXTM3U
#EXTINF:-1 
rtsp://127.0.0.1:8554

このm3uファイルをkodiで開くとshowroom動画が再生されました。