クリエイター:メタボ兔

ウェブやアプリの開発者で利用する色な技術やサーバーや開発環境の設定について共有する場

iTermの背景にsshログイン情報を表示

f:id:FattyRabbit:20210201221004j:plain

概要

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です。

利用可能範囲

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

stackoverflow.com

現在右上に情報が見えるようにしていますが、左上に表示するようにすれば解決されるかなと思います。ただ、邪魔でした。(ㅠㅠ)

~/.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サーバーが認識されなくなった件

f:id:FattyRabbit:20210115185844j:plain

MacのOSをBig Surにアップデートした後に色な問題が発生しています。その中で一つを紹介&対応方法を生理して見ました。

環境設定

Raspberry Pi 3+にDockerコンテナを構築してウェブ、DBAなどのイントラネットを構築してそのウェブサービスドメインを整理してDnsmasqのコンテナに登録、LocalのネットにDNSサーバーとして登録している状態です。

参考: https://fatty-rabbit.hatenablog.com/entry/2020/02/07/000000

fatty-rabbit.hatenablog.com

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

f:id:FattyRabbit:20210105173132p:plain

# 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

湯飲みで盆栽の鉢を作る

材料

f:id:FattyRabbit:20201230151928j:plain

  • ヤスリ:角が丸い、100円ショップ(ダイアモンド)
  • 湯飲み:100円ショップ
  • ネジ、釘:家にあるもの
  • ゴムハンマー:家にあるもの(100円ショップ)

湯飲みの選別は底の厚みが薄い物を探しました。

穴あけ

他のネットの記事を読むと湯飲みの中に砂を入れて釘とハンマーで穴を開ける内容がありましたが、砂もないし面倒かなと思いまして底が薄い物に協力なテープを貼ってやりました。

f:id:FattyRabbit:20201230152821j:plainf:id:FattyRabbit:20201230152845j:plain

間違えて割れるのではないか心配もしましたが、何と一発で成功しました。

穴を広げる

盆栽用の鉢は普通の物より水捌けをよくするように穴が大きいです。今回ネジで開けた穴は小さかったのでヤスリで穴を広げました。

f:id:FattyRabbit:20201230153445j:plainf:id:FattyRabbit:20201230153517j:plain

また、その横にも水が流れるように三つのボコを作りました。もっと大きくした方が良いかと思いながら、このくらいにするのも大変だったので手を上げました。

完成

以前買った松の寄せ植えから1本分けて使ってみました。

f:id:FattyRabbit:20201230153946j:plain

GAS(Google Apps Script)を利用してQiitaとNoteの記事をSlackへ送信

事前準備

SlackのAPIを使うためにTokenが必要です。基本的に二つの物があります。

Legacy token

workspaceの管理用のTokenなので、権限のレベルが高いので他人に共有することなら良く考えた方が良いです。

参考:https://qiita.com/ykhirao/items/0d6b9f4a0cc626884dbb

qiita.com

App Token

作成したアプリに制限されるのでそのアプリを追加したチャネルのみ影響されます。

参考:https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21

qiita.com

今回は自分はアプリを作成して、そのアプリ対象のチャネルに登録し利用しました。

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です。

アプリの場合は対象チャネルでマウスの右ボタンをクリックすると以下のメニューが表示されます。

f:id:FattyRabbit:20201230131656p:plain

このメニューの「リンクをコピー」をクリックすると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パターンです。

トリガーの設定

「編集」 > 「現在のプロジェクトのトリガー」を選択してトリガー作成を行います。

f:id:FattyRabbit:20201230131927p:plain

時間設定は分単位で「LIMIT_TIME_LAG」の設定と合わせて5分に設定します。

f:id:FattyRabbit:20201230131956p:plain

記憶しておくべき

権限(SCOPE)

アプリのアイコンや表示名の変更する以下のソースがありますが、こちら基本的に必要な権限(SCOPE)があります。

参考:https://api.slack.com/methods/chat.postMessage

api.slack.com

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する方法が欲しいですね。

参考:https://qiita.com/jerrywdlee/items/a037bb7764b0671d4059

qiita.com