iTermの背景にsshログイン情報を表示
概要
iTerm2をsshクライアントツールで愛用していますが、たまに複数のサーバーへ接続しているとどこがなんなのか分からなくなる場合があります。
その場合、接続情報を可視化したら良いかと思いまして色々調べて改善した内容です。(まだ、課題点はあり〜〜〜)
いきなりソース
自分も色々なところのソースを利用したので一旦ソースを共有した方が良いかと思います。
「~/bin/ssh-change-bg.sh」を作成します。
#!/bin/bash # SSH with host name and IP address in background (only in iTerm.app) # First, check to see if we have the correct terminal! if [ "$(tty)" == 'not a tty' ] || [ "$TERM_PROGRAM" != "iTerm.app" ] ; then /usr/bin/ssh "$@" exit $? fi function __calculate_iterm_window_dimensions { local size=( $(osascript -e " tell application \"System Events\" tell application process \"iTerm2\" get size of window 1 end tell end tell" | tr ',' ' ') ) local w=$(( ${size[0]} - 15 )) h=$(( ${size[1]} - 44 )) echo "${w}x${h}" } # Console dimensions DIMENSIONS=$(__calculate_iterm_window_dimensions) BG_COLOR="#000000" # Background color FG_COLOR="#C68080" # Foreground color GRAVITY="NorthEast" # Text gravity (NorthWest, North, NorthEast, # West, Center, East, SouthWest, South, SouthEast) OFFSET1="20,10" # Text offset OFFSET2="20,80" # Text offset FONT_SIZE="60" # Font size in points FONT_STYLE="Normal" # Font style (Any, Italic, Normal, Oblique) # Font path FONT="$HOME/.bash/resources/SimpleLife.ttf" HOSTNAME=`echo $@ | sed -e "s/.*@//" -e "s/ .*//"` configInfo=`pcregrep -M 'Host '$HOSTNAME'([\s\S](?!Host ))+' ~/.ssh/config` if [ ! -z "$configInfo" ]; then RESOLVED_HOSTNAME=$HOSTNAME CONFIG_HOSTNAME=`echo -e "$configInfo" | grep -oEi 'hostname\s+(.+)' | sed -e "s/ \+/ /g" |cut -f2 -d" "` output=`dscacheutil -q host -a name $CONFIG_HOSTNAME` RESOLVED_IP=`echo -e "$output"|grep '^ip_address:'|awk '{print $2}'` RESOLVED_USER=`echo -e "$configInfo" | grep -oEi 'user\s+(\S+)' | sed -e "s/ \+/ /g" |cut -f2 -d" "`"@" else output=`dscacheutil -q host -a name $HOSTNAME` RESOLVED_HOSTNAME=`echo -e "$output"|grep '^name:'|awk '{print $2}'` RESOLVED_IP=`echo -e "$output"|grep '^ip_address:'|awk '{print $2}'` fi function set_bg { local tty=$(tty) osascript -e " tell application \"iTerm2\" tell current session of current window set background image to \"$1\" end tell end tell" } on_exit () { if [ ! -f /tmp/iTermBG.empty.png ]; then convert -size "$DIMENSIONS" xc:"$BG_COLOR" "/tmp/iTermBG.empty.png" fi set_bg "/tmp/iTermBG.empty.png" rm "/tmp/iTermBG.$$.png" } trap on_exit EXIT convert \ -size "$DIMENSIONS" xc:"$BG_COLOR" -gravity "$GRAVITY" -fill "$FG_COLOR" -style "$FONT_STYLE" -pointsize "$FONT_SIZE" -antialias -draw "text $OFFSET1 '${RESOLVED_HOSTNAME:-$HOSTNAME}'" \ -pointsize 30 -draw "text $OFFSET2 '${RESOLVED_USER}${RESOLVED_IP:-}'" -alpha Off \ "/tmp/iTermBG.$$.png" set_bg "/tmp/iTermBG.$$.png" /usr/bin/ssh "$@"
実装権限を与えます。
% chmod +x ~/bin/ssh-change-bg.sh
~/.bash_profileに以下を追記してsshコマンドでssh-change-bg.shが実行されるようにします。
alias ssh='~/bin/ssh-change-bg.sh'
~/.bash_profileを読み込みましょう。
% source ~/.bash_profile
ソースを見るとわかる方もいるかと思いますが、「convert」を使用するために「ImageMagick」をインストールする必要があります。
ImageMagickのインストール
思ったより簡単です。
% brew install imagemagick
一応converterコマンドの参考URLです。
- ImageMagick: Convert, Edit, Or Compose Bitmap Images
- ImageMagick: Tools (how to use ImageMagick) www.imagemagick.org
利用可能範囲
iTerm2のPaneサイズの取得不可
現在のソースの「__calculate_iterm_window_dimensions」部分ですが、Window(Or Tab)のpixelサイズしか取得できない状態です。Apple Script?的な仕様上paneとかsessionにはSizeの属性がないためです。
sttyコマンドでライン数やカラム数の取得できますが、それをpixelで取得する方法がまだ見つかってないです。
That's stty size | cut -d" " -f1 for the height/lines and stty size | cut -d" " -f2 for the width/columns
現在右上に情報が見えるようにしていますが、左上に表示するようにすれば解決されるかなと思います。ただ、邪魔でした。(ㅠㅠ)
~/.ssh/configの参照
~/.ssh/configで定義されたパターン(自分が使っているパターン)を読み込んで情報として利用するようにしています。
こんな感じです。大文字小文字、英数字とかが関係すると思います。
Host raspi-of-local HostName 172.16.221.18 IdentityFile ~/.ssh/id_rsa User pi
上記のソースはここら辺です。
configInfo=`pcregrep -M 'Host '$HOSTNAME'([\s\S](?!Host ))+' ~/.ssh/config` if [ ! -z "$configInfo" ]; then RESOLVED_HOSTNAME=$HOSTNAME RESOLVED_IP=`echo -e "$configInfo" | grep -oE 'HostName\s+([0-9\.]+)' | sed -e "s/ \+/ /g" |cut -f2 -d" "` RESOLVED_USER=`echo -e "$configInfo" | grep -oE 'User\s+(\S+)' | sed -e "s/ \+/ /g" |cut -f2 -d" "`"@" else ... fi
課題
やっぱりpaneのサイズを正確に取得したいですね。ww
Mac Big SurでDnsmasqサーバーが認識されなくなった件
MacのOSをBig Surにアップデートした後に色な問題が発生しています。その中で一つを紹介&対応方法を生理して見ました。
環境設定
Raspberry Pi 3+にDockerコンテナを構築してウェブ、DBAなどのイントラネットを構築してそのウェブサービスのドメインを整理してDnsmasqのコンテナに登録、LocalのネットにDNSサーバーとして登録している状態です。
参考: https://fatty-rabbit.hatenablog.com/entry/2020/02/07/000000
Macの設定
resolverの登録を行いました。例えばraspberryのIP(dnsmasqがexposeした)が「172.16.221.18」の場合
% sudo mkdir /etc/resolver/ % sudo vi /etc/resolver/localnet.intra search local nameserver 172.16.221.18
また、ネットワークの設定で以下の順でDNSを登録
172.16.221.18 8.8.8.8
Dnsmasqの設定
先ずはdnsmasq.confです。
# Do not use /etc/hosts as nameserver no-resolv # Use this file as a hosts file addn-hosts=/etc/hosts-dnsmasq # プライベートIPアドレスの逆引を上位のDNSへ問い合わせない(無駄なので) bogus-priv # Log all dns queries log-queries
マックのネットワークのDNSに8.8.8.8を設定しているので、Upstream DNS Serverの「server」設定はしてないです。
hosts-dnsmasqの内容です。
127.0.0.1 local 127.0.0.1 mailhog.local 127.0.0.1 a.local 127.0.0.1 b.local
これで実際digやping、ブラウザで表示が正常に表示されていました。
Big Surのアップデート後の現象
ブラウザとpingで本当にたまに反応があるだけで殆ど使用していたlocalドメインが認識できなくなりました。digでDNSサーバーとして正常に動いていることは確認できます。
対応方法
理由が分からないですがので解決方法だけ書くのが恥ずかしいですが、一応書くと以下の行をdnsmasq.confに追加追加するだけです。
+ # Upstream DNS Server + server=8.8.8.8
理由が分かる方是非連絡ください。
CentOS6のサポート終了の対応(yum使用出来るように)
問題点
CentOS6のサポートが終了(2020-11-30)されたことでCentOSのバージョンアップをしてないサーバーのyumの使用が出来なくなりました。
https://wiki.centos.org/About/Product
# yum check-update 読み込んだプラグイン:fastestmirror, refresh-packagekit, security Determining fastest mirrors YumRepo Error: All mirror URLs are not using ftp, http[s] or file. Eg. Invalid release/repo/arch combination/ removing mirrorlist with no valid mirrors: /var/cache/yum/x86_64/6/base/mirrorlist.txt エラー: Cannot find a valid baseurl for repo: base
ところでサポートが終わってもモジュールの追加が必要などき何もインストール出来ないのは問題ですね。
対策
リポジトリのURLを書き換えることで対処する。
# sed -i -e "s|mirror\.centos\.org/centos/\$releasever|vault\.centos\.org/6.6|g" /etc/yum.repos.d/CentOS-Base.repo # sed -i -e "s|#baseurl=|baseurl=|g" /etc/yum.repos.d/CentOS-Base.repo # sed -i -e "s|mirrorlist=|#mirrorlist=|g" /etc/yum.repos.d/CentOS-Base.repo
キャッシュをクリアする。
# yum clean all
出来るはずだと思いましたが、エラーが出ますね。
Cannot retrieve metalink for repository: epel. Please verify its path and try again
epel.repoが問題らしいです。
# sed -i "s/mirrorlist=https/mirrorlist=http/" /etc/yum.repos.d/epel.repo
キャッシュをクリアして使用します。
# yum clean all # yum update
湯飲みで盆栽の鉢を作る
材料
- ヤスリ:角が丸い、100円ショップ(ダイアモンド)
- 湯飲み:100円ショップ
- ネジ、釘:家にあるもの
- ゴムハンマー:家にあるもの(100円ショップ)
湯飲みの選別は底の厚みが薄い物を探しました。
穴あけ
他のネットの記事を読むと湯飲みの中に砂を入れて釘とハンマーで穴を開ける内容がありましたが、砂もないし面倒かなと思いまして底が薄い物に協力なテープを貼ってやりました。
間違えて割れるのではないか心配もしましたが、何と一発で成功しました。
穴を広げる
盆栽用の鉢は普通の物より水捌けをよくするように穴が大きいです。今回ネジで開けた穴は小さかったのでヤスリで穴を広げました。
また、その横にも水が流れるように三つのボコを作りました。もっと大きくした方が良いかと思いながら、このくらいにするのも大変だったので手を上げました。
完成
以前買った松の寄せ植えから1本分けて使ってみました。
GAS(Google Apps Script)を利用してQiitaとNoteの記事をSlackへ送信
事前準備
SlackのAPIを使うためにTokenが必要です。基本的に二つの物があります。
Legacy token
workspaceの管理用のTokenなので、権限のレベルが高いので他人に共有することなら良く考えた方が良いです。
参考:https://qiita.com/ykhirao/items/0d6b9f4a0cc626884dbb
App Token
作成したアプリに制限されるのでそのアプリを追加したチャネルのみ影響されます。
参考:https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21
今回は自分はアプリを作成して、そのアプリ対象のチャネルに登録し利用しました。
GAS作成
Google Driveで「新規」 > 「その他」 > 「Google App Script」を選択して、新しいGASを作成します。
完成ソース
いきなり完成ソースを共有します。その後に細かい説明します。
// Slack定数 var SLACK_CANNEL = "SlackのChannel"; var SLACK_URL = 'https://slack.com/api/chat.postMessage'; var SLACK_TOKEN = 'xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // RSS定数 var FORMAT_TITLE = "%sに新しい投稿があります。"; var FORMAT_QIITA_URL = "http://qiita.com/organizations/%s/activities.atom"; var FORMAT_QIITA_AUTHOR_URL = "http://qiita.com/%s"; var FORMAT_NOTE_URL = "https://note.com/%s/rss"; var FORMAT_NOTE_AUTHOR_URL = "https://note.com/%s"; var KIND_QIITA = "Qiita"; var KIND_NOTE = "Note"; // 5分をミリ秒へ変換したもの var LIMIT_TIME_LAG = 300000; // 更新通知を行うユーザー名 var organizations = [ {kind: KIND_QIITA, key: "Qiitaの企業のURLキー"}, {kind: KIND_NOTE, key: "NoteのURLキー"} ]; var exceptions = []; function main() { // 記事をSlackへ送信 for (var k = 0; k < organizations.length; k++) { parseXml(organizations[k]); } // Exceptionを出力 if (exceptions.length > 0) { for(var i = 0; i < exceptions.length; i++) { var e = exceptions[i]; Logger.log(e); } throw new Error('RSSの通信のエラーがありました。ログを確認してください。'); } } function parseXml(rssInfo) { var url = null, authorUrlFormat = null; var blocks = [], posts = []; var msgTitle = Utilities.formatString(FORMAT_TITLE, rssInfo.kind); if (rssInfo.kind == KIND_QIITA) { // Qiitaの場合 // フィードURL url = Utilities.formatString(FORMAT_QIITA_URL, rssInfo.key); // 作成者URL authorUrlFormat = FORMAT_QIITA_AUTHOR_URL; // Blockの初期(先頭)設定 blocks = [ { type: "section", text: { type: "mrkdwn", text: msgTitle + "\n\n*新しい記事リスト*" } }, { "type": "divider" } ]; posts = parseQiita(url); } else if (rssInfo.kind == KIND_NOTE) { // Noteの場合 // フィードURL url = Utilities.formatString(FORMAT_NOTE_URL, rssInfo.key); // 作成者URL authorUrlFormat = FORMAT_NOTE_AUTHOR_URL; // Blockの初期(先頭)設定 blocks = [ { type: "section", text: { type: "mrkdwn", text: msgTitle + "\n\n*新しい記事リスト*" } }, { "type": "divider" } ]; posts = parseNote(url); } if (posts.length > 0) { for (var i = 0; i < posts.length; i++) { var post = posts[i]; msgTitle = msgTitle + " <" + post.url + ">"; blocks.push( { type: "section", text: { type: "mrkdwn", text: "<" + Utilities.formatString(authorUrlFormat, post.authorKey) + "|@" + post.authorName + ">から「" + post.title + "」が投稿されました。\nLink: <" + post.url + ">" } } ); } // Blockの最後設定 blocks.push( { "type": "divider" }); var tmp = JSON.stringify(blocks); // Logger.log(tmp); var payload = { token: SLACK_TOKEN, channel: SLACK_CANNEL, text: msgTitle, unfurl_links: true, username: "Wiz Robot", icon_url: "https://inspirelabs.pl/wp-content/uploads/2018/01/robot.png", blocks: JSON.stringify(blocks) }; var params = { 'method': 'post', 'payload': payload, 'muteHttpExceptions': true }; try { var response = UrlFetchApp.fetch(SLACK_URL, params); } catch(e) { exceptions.push(e); } var responseCode = response.getResponseCode(); if (responseCode != 200) { var responseBody = response.getContentText(); exceptions.push(Utilities.formatString("Request failed. Expected 200, got %d: %s", responseCode, responseBody)); } } } function parseQiita(url) { // 上記URLにGETリクエスト、getContentText()でxmlを取得 var response = UrlFetchApp.fetch(url, {'muteHttpExceptions': true}); // 取得失敗した場合 var responseCode = response.getResponseCode(); if (responseCode != 200) { var responseBody = response.getContentText(); exceptions.push(Utilities.formatString("Request failed. Qiita Expected 200, got %d: %s", responseCode, responseBody)); return []; } // xmlを取得 var xml = response.getContentText(); // 取得したxmlを文書化 var document = XmlService.parse(xml); // ドキュメントのルート要素を抽出 var root = document.getRootElement(); // 引数のURLを用いてNameSpaceを生成します。(おそらくフォーマット指定?) var atom = XmlService.getNamespace("http://www.w3.org/2005/Atom"); // エントリー記事を取得 var entries = root.getChildren("entry", atom); // Logger.log(entries); var returnArray = []; for (var i = 0; i < entries.length; i++) { // 最新記事を取得 var entry = entries[i]; var id = entry.getChild("id", atom).getText(); var pos = id.indexOf("/"); var entrieId = id.substr(pos+1); // 記事情報の取得 var postUrl = entry.getChild("url", atom).getText(); var title = entry.getChild("title", atom).getText(); var author = entry.getChild("author", atom).getChild("name", atom).getText(); // 記事の更新日時を取得 var publishTime = entry.getChild("published", atom).getText(); var nowTime = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy-MM-dd'T'HH:mm:ss"); // 更新があった場合[1分以内に更新があったもの] if (Date.parse(nowTime) - Date.parse(publishTime) <= LIMIT_TIME_LAG) { returnArray.push({authorKey: author, authorName: author, title: title, url: postUrl}); } } return returnArray; } function parseNote(url) { // 上記URLにGETリクエスト、getContentText()でxmlを取得 var response = UrlFetchApp.fetch(url, {'muteHttpExceptions': true}); // 取得失敗した場合 var responseCode = response.getResponseCode(); if (responseCode != 200) { var responseBody = response.getContentText(); exceptions.push(Utilities.formatString("Request failed. Note Expected 200, got %d: %s", responseCode, responseBody)); return []; } // xmlを取得 var xml = response.getContentText(); // 取得したxmlを文書化 var document = XmlService.parse(xml); // XMLのitem配下のデータを取得 var items = document.getRootElement().getChildren('channel')[0].getChildren('item'); // Logger.log(items); // 引数のURLを用いてNameSpaceを生成します。 var node = XmlService.getNamespace("https://note.com"); var returnArray = []; for (var i = 0; i < items.length; i++) { // 最新記事を取得 var item = items[i]; // 記事情報の取得 var postUrl = item.getChild("link").getText(); var title = item.getChild("title").getText(); var authorKey = item.getChild("link").getText().match(/https:\/\/note.com\/([a-z0-9_]*?)\/.*?/)[1]; var authorName = item.getChild("creatorName", node).getText(); // 記事の更新日時を取得 var publishTime = item.getChild("pubDate").getText(); var nowTime = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy-MM-dd'T'HH:mm:ss"); // 更新があった場合[1分以内に更新があったもの] if (Date.parse(nowTime) - Date.parse(publishTime) <= LIMIT_TIME_LAG) { returnArray.push({authorKey: authorKey, authorName: authorName, title: title, url: postUrl}); } } return returnArray; }
SLACK_CHANNEL
ブラウザーだとURLが「https://xxxxxx.slack.com/archives/YYYYYYYYY」の場合、「YYYYYYYYY」がチャネルIDです。
アプリの場合は対象チャネルでマウスの右ボタンをクリックすると以下のメニューが表示されます。
このメニューの「リンクをコピー」をクリックするとURLが見えるので同じく一番後ろの「YYYYYYYYY」を取得します。
SLACK_TOKEN
上記「事前準備」のどちらかのTokenを設定します。
FORMAT_QIITA_URL
Qiitaの企業向けのRSS配信するURLパターンです。「%s」のところに企業URL「https://qiita.com/organizations/xxxxx」の一番後ろの「xxxxx」が入る感じです。この「xxxxx」が「Qiitaの企業のURLキー」部分に入ります。
FORMAT_QIITA_AUTHOR_URL
RSSをパーシングして作成者の情報から実際の作成者のURLを作るURLパターンです。
FORMAT_NOTE_URL
FORMAT_QIITA_URLと同様にNoteのRSS配信するURLパターンです。「http://note.com/」と「/rss」の間の文字が「NoteのURLキー」部分に入ります。
FORMAT_NOTE_AUTHOR_URL
FORMAT_QIITA_AUTHOR_URLと同様に作成者の情報から実際の作成者のURLを作るURLパターンです。
トリガーの設定
「編集」 > 「現在のプロジェクトのトリガー」を選択してトリガー作成を行います。
時間設定は分単位で「LIMIT_TIME_LAG」の設定と合わせて5分に設定します。
記憶しておくべき
権限(SCOPE)
アプリのアイコンや表示名の変更する以下のソースがありますが、こちら基本的に必要な権限(SCOPE)があります。
参考:https://api.slack.com/methods/chat.postMessage
OGP カード
「unfurl_links: true」にしてもOGP情報が表示されなくて、調べた結果textに設定したもので6個以上は表示されないようです。そのために以下の部分を入れました。
msgTitle = msgTitle + " <" + post.url + ">";
その後にmsgTitleを設定します。このtextが実際表示されるのはOS(?)の通知に表示されるだけです。
var payload = { token: SLACK_TOKEN, channel: SLACK_CANNEL, text: msgTitle, unfurl_links: true, username: "Robot", icon_url: "https://XXXXXXXX/robot.png", blocks: JSON.stringify(blocks) };
Qiitaのエラー
実際運営して見るとQiitaのRSSで以下のようなエラーが出ていました。
Exception: 使用できないアドレス: http://qiita.com/organizations/xxxxxx/activities.atom
5分単位でトリガーを設定したので、負荷?が掛かったのか良く分からないですがその後の処理が実行されなくなるので、以下の部分で対応しました。
// 上記URLにGETリクエスト、getContentText()でxmlを取得 var response = UrlFetchApp.fetch(url, {'muteHttpExceptions': true}); // 取得失敗した場合 var responseCode = response.getResponseCode(); if (responseCode != 200) { var responseBody = response.getContentText(); exceptions.push(Utilities.formatString("Request failed. Qiita Expected 200, got %d: %s", responseCode, responseBody)); return []; } // xmlを取得 var xml = response.getContentText();
「muteHttpExceptions: true」に設定した後にresponseCodeで判定してエラーのログを出力します。
改善すべきところ
Function的な感じ
全然OOPではないことに完成ソースを見るとめがチクチクして来ました。ソースを管理する上にlocalでTSで作成して管理、実行時にBuildする方法が欲しいですね。