speg03の雑記帳

主に未来の自分のために試したことなどを記録しています

PythonでDNSの正引きと逆引き

諸般の事情からスクリプトでホスト名とIPアドレスの対応を取りたいことがありますのでメモしておきます。

socket

socket --- 低水準ネットワークインターフェイス — Python 3.8.3 ドキュメント

標準ライブラリで実現できるシンプルな方法です。

import socket

# 正引き
socket.gethostbyname("www.google.com")
#=> '172.217.31.164'

# 逆引き
socket.gethostbyaddr("172.217.31.164")
#=> ('nrt12s22-in-f4.1e100.net', ['164.31.217.172.in-addr.arpa'], ['172.217.31.164'])

逆引きの結果はタプルになっていて、最初の値がホスト名です。

dnspython

dnspython home page

高度な機能を使うにはこちらのようなライブラリを使うとよさそうです。本稿執筆時点のバージョンは1.16.0です。

$ pip install dnspython

socketと同じことをやってみます。

from dns import resolver, reversename

# 正引き
answers = resolver.query("www.google.com")
#=> <dns.resolver.Answer object at 0x104829050>
[answer.to_text() for answer in answers]
#=> ['172.217.174.100']

# 逆引き
answers = resolver.query(reversename.from_address("172.217.174.100"), "PTR")
#=> <dns.resolver.Answer object at 0x104eebfd0>
[answer.to_text() for answer in answers]
#=> ['nrt12s28-in-f4.1e100.net.']

クエリの結果は複数の場合があるのでリストとして扱っています。to_text関数を使って値を文字列として取り出すことができます。

逆引きするには、IPアドレスを逆向きにしたようなホスト名をPTRレコードとして問い合わせます。reversename.from_addressが逆向きのホスト名を作る関数です。次のような値になっています。

reversename.from_address("172.217.174.100")
#=> <DNS name 100.174.217.172.in-addr.arpa.>

さらに様々な理由によって、問い合わせ先のネームサーバーの指定が必要だとします。これは(おそらく)socketでは素直にできないです。

新しいresolverを作ってそれにネームサーバーを設定します。あとはそれを使って先ほどと同じようにquery関数を呼ぶだけです。

res = resolver.Resolver()
res.nameservers = ["8.8.8.8"]
answers = res.query("www.google.com")

xonshことはじめ

最近、xonshを使い始めました。

xonshrc

xonshの設定ファイルは~/.config/xonsh/rc.xshや~/.xonshrcが読み込まれるようですが、私は設定ファイルを何でも~/.configに集めたいので前者のファイルを作っています。

$COMPLETIONS_CONFIRM = True
$HISTCONTROL = "ignoredups"
$XONSH_AUTOPAIR = True

$DYNAMIC_CWD_WIDTH = (30, "c")
$PROMPT = (
    "{BOLD_GREEN}{user}@{hostname} {BOLD_BLUE}{cwd}{NO_COLOR} {gitstatus}\n"
    "{env_name}{prompt_end} "
)

@events.on_ptk_create
def custom_keybindings(bindings, **kw):

    @bindings.add("c-@")
    def _change_repository(event):
        repository = $(ghq list -p | peco).strip()
        if repository:
            cd @(repository)
            event.current_buffer.validate_and_handle()

環境変数による設定やプロンプトの設定はあまり凝ったことはせずシンプルなものです。重要なのはzshのときからずっと使っているghq+pecoの操作をショートカットキーで呼び出せるようにすることです。

c-@のキーバインドはc-spaceのエイリアスになっていて*1、実際にはc-spaceで入力しています。

$(ghq list -p | peco).strip() はシェルで実行されたghq+pecoの結果をPython文字列として処理しています(改行文字が末尾についているのでstrip)。

cd @(repository)Python文字列をシェルのコマンドラインに渡すための記法です。シェルとPythonを気軽に行ったり来たりできるのは便利ですね。

最後の event.current_buffer.validate_and_handle() *2 は現在のプロンプトにENTERキーを入力するようなもので、cdした結果がすぐにプロンプトに反映されるようにしています。

xonshの起動方法

なんとなくログインシェルに設定するには抵抗があるので、zshから起動して使います。

具体的には、zshenvにPATHなどの最低限の設定を書いておいて、zshrcからtmuxを起動し、tmuxのデフォルトシェルとしてxonshが起動するようにしています。

if [[ $TERM_PROGRAM != vscode && -z $XONSH_VERSION && -z $TMUX ]]; then
    run-tmux
fi

条件 $TERM_PROGRAM != vscodeVS Codeのターミナルで起動していないことを確認しています。VS CodePythonを書いているときにターミナルを開くと、Python仮想環境のactivateが自動的に実行されますが、それがxonshではうまく動作しません。そのため、VS Codeのターミナルではtmuxも使わず素のzshを使うようにしています。

条件 -z $XONSH_VERSION はxonshからzshが実行されていないことを確認しています。直接zshを実行した場合以外にも、xonshのsource-zshを実行した場合も該当します。

条件 -z $TMUX はtmuxが実行中でないことを確認しています。

スクリプト run-tmux はすでに起動中のtmuxセッションがあればそれに接続し、なければ新規のセッションにつなぐラッパースクリプトです。 ちなみに次のようなBashスクリプトになっています。

#!/usr/bin/env bash

session=${1:-main}

if [[ -z $TMUX ]]; then
    tmux new-session -A -s "$session"
else
    if ! tmux has-session -t "$session" &>/dev/null; then
        TMUX='' tmux new-session -d -s "$session"
    fi
    tmux switch-client -t "$session"
fi

これでひとまずは不自由なくxonsh生活が送れそうです。

*1:xonshが使っているprompt_toolkitのドキュメントに記載があります

*2:prompt_toolkitのBufferクラスからそれっぽい関数を見つけました

Visual Studio CodeでPython仮想環境

かれこれ10年以上ぐらい使っていたEmacsからVisual Studio Code(以下、VS Code)に改宗する可能性を感じて、ひとまず最近よく書いているPython環境をVS Codeで整えようという話です。

Pythonはプロジェクトごとの仮想環境の扱いがうまくいけばとりあえず何とかなるだろうという感じです。

私のPython仮想環境の作り方

私のPython仮想環境の作り方はこちらです。 sampleはプロジェクトの名前などです。プロジェクトのディレクトリ以下の.venvの下に仮想環境を作ります。

python3 -m venv .venv/sample

ところが、巷でよく見かける仮想環境の作り方は単に次のようにしているようです。

python3 -m venv .venv

この方法で私がイマイチだなぁと思っているのは、仮想環境をactivateしたときに表示される名前が.venvになってしまうことです。

speg03@speg03-mbp sample % python3 -m venv .venv
speg03@speg03-mbp sample % . .venv/bin/activate
(.venv) speg03@speg03-mbp sample %

そのため最初に書いたように仮想環境を作っています。

speg03@speg03-mbp sample % python3 -m venv .venv/sample
speg03@speg03-mbp sample % . .venv/sample/bin/activate
(sample) speg03@speg03-mbp sample %

こうなっていると複数の仮想環境を作ったときでも、今どの仮想環境を使っているのかわかりやすくなります。

VS Codeの設定

VS CodePythonのファイルを開いたときに、どのPythonを使うか選択することができます。しかし、.venvに仮想環境を作っていた場合はその中のPythonを見つけてくれるようですが、.venv/sampleのように作っていた場合は見つけてくれないようでした。

VS Codeの設定ではpython.venvPathに仮想環境を作っているディレクトリを指定してあげればよいようです。プロジェクトのワークスペース直下というのは${workspaceFolder}で表現できます。

{
    "python.venvPath": "${workspaceFolder}/.venv"
}

一度、この中にある仮想環境を選択しておけば、ワークスペース設定として、.vscode/settings.jsonにそのPythonのパスが書き込まれているので、次回以降はこの仮想環境が自動的に選ばれるようになります。

参考: Using Python Environments in Visual Studio Code

pytestでコンソールアプリケーションのテストを書く

テスト対象のアプリケーション

コンソールアプリケーションの題材として、ここでは以下のようなアプリケーションを考えます。

  • プレースホルダーが1つ含まれたフォーマット文字列を標準入力から受け取る
  • コマンドライン引数としてnameを受け取る
  • フォーマット文字列にnameの値を埋め込んだ結果を標準出力へ出力する

次のように実装しました。

import argparse
import sys


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--name")
    args = parser.parse_args()

    template = sys.stdin.read()
    print(template.format(args.name))


if __name__ == "__main__":
    main()

実行結果は以下のようになります。

echo "hello, {}" | python3 application.py --name=world
hello, world

テスト

pytestを使ってこのアプリケーションのテストを書いてみます。ポイントとなるのはコマンドライン引数、標準入力、標準出力をどのように扱えばよいかという点です。

import io

from application import main


def test_main(monkeypatch):
    argv = ["application.py", "--name=world"]
    stdin = io.StringIO("hello, {}")
    stdout = io.StringIO()

    with monkeypatch.context() as m:
        m.setattr("sys.argv", argv)
        m.setattr("sys.stdin", stdin)
        m.setattr("sys.stdout", stdout)
        main()

    assert stdout.getvalue() == "hello, world\n"

テストが成功することを確認します。

pytest test_application.py

このテストコードについて、もう少し見ていきましょう。

コマンドライン引数

    args = parser.parse_args()

この部分で内部的にsys.argvの値を参照しています。sys.argvコマンドライン引数の配列です。最初の要素はスクリプトの名前になっており、その後ろに実際のコマンドライン引数が続きます。

標準入力

    template = sys.stdin.read()

sys.stdinから標準入力の文字列を取得しています。sys.stdinはファイルオブジェクトです。

標準出力

    print(template.format(args.name))

printが実行されるとsys.stdoutに対して文字列の書き込みが発生します。sys.stdoutsys.stdinと同様にファイルオブジェクトです。

Amazon Linuxの最新AMIを取得する

概要

Amazon Linuxの最新AMIを取得する方法について説明します。AMIのIDはリージョンごとに異なるため、利用しているリージョンで有効なAMIが取得できる方法を考えます。

事前準備

次のようなコードでEC2のAPIにアクセスできる状態とします。

import boto3

session = boto3.session.Session()
ec2 = session.client('ec2')

実験

現時点で、Amazon Linuxの最新AMIはamzn-ami-hvm-2017.09.1.20180115-x86_64-gp2という名前のようです。(東京リージョンではID ami-ceafcba8です)

query = 'amzn-ami-hvm-2017.09.1.20180115-x86_64-gp2'
response = ec2.describe_images(Filters=[{'Name': 'name', 'Values': [query]}])
response['Images'][0]
{'Architecture': 'x86_64',
 'BlockDeviceMappings': [{'DeviceName': '/dev/xvda',
   'Ebs': {'DeleteOnTermination': True,
    'Encrypted': False,
    'SnapshotId': 'snap-0d38721ac6a1fdcc9',
    'VolumeSize': 8,
    'VolumeType': 'gp2'}}],
 'CreationDate': '2018-01-15T19:13:56.000Z',
 'Description': 'Amazon Linux AMI 2017.09.1.20180115 x86_64 HVM GP2',
 'EnaSupport': True,
 'Hypervisor': 'xen',
 'ImageId': 'ami-ceafcba8',
 'ImageLocation': 'amazon/amzn-ami-hvm-2017.09.1.20180115-x86_64-gp2',
 'ImageOwnerAlias': 'amazon',
 'ImageType': 'machine',
 'Name': 'amzn-ami-hvm-2017.09.1.20180115-x86_64-gp2',
 'OwnerId': '137112412989',
 'Public': True,
 'RootDeviceName': '/dev/xvda',
 'RootDeviceType': 'ebs',
 'SriovNetSupport': 'simple',
 'State': 'available',
 'VirtualizationType': 'hvm'}

フィルタリングを使って検索してみます。 https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/Using_Filtering.html

query = 'amzn-ami-hvm-????.??.?.????????-x86_64-gp2'
response = ec2.describe_images(
    Filters=[{'Name': 'name', 'Values': [query]}],
    Owners=['amazon'])

# 作成日の降順でソート
images = sorted(response['Images'], key=lambda img: img['CreationDate'], reverse=True)

# AMI名だけのリストで表示
[img['Name'] for img in images]
['amzn-ami-hvm-2017.09.1.20180115-x86_64-gp2',
 'amzn-ami-hvm-2017.09.1.20180108-x86_64-gp2',
 'amzn-ami-hvm-2017.09.1.20180103-x86_64-gp2',
 'amzn-ami-hvm-2017.09.1.20171120-x86_64-gp2',
 'amzn-ami-hvm-2017.09.1.20171103-x86_64-gp2',
 'amzn-ami-hvm-2017.09.0.20170930-x86_64-gp2',
 'amzn-ami-hvm-2017.03.1.20170812-x86_64-gp2',
 'amzn-ami-hvm-2017.03.1.20170623-x86_64-gp2',
 'amzn-ami-hvm-2017.03.1.20170617-x86_64-gp2',
 'amzn-ami-hvm-2017.03.0.20170417-x86_64-gp2',
 'amzn-ami-hvm-2017.03.0.20170401-x86_64-gp2',
 'amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2',
 'amzn-ami-hvm-2016.09.1.20161221-x86_64-gp2',
 'amzn-ami-hvm-2016.09.0.20161028-x86_64-gp2',
 'amzn-ami-hvm-2016.09.0.20160923-x86_64-gp2']

botocoreを使ったAWS設定情報の取得

概要

AWSのcredentialsファイルやconfigファイルの設定情報をbotocoreを使って取得する方法を説明します。また、それらの設定ファイルには独自の項目を追加できます。

AWS設定情報の取得

AWSのcredentials, configが次のような内容だったとします。

[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
[default]
region = ap-northeast-1

[profile prodaccess]
region = ap-northeast-1
role_arn = arn:aws:iam::123456789012:role/ProductionAccessRole
source_profile = default

botocoreを使って設定情報を簡単に取得することができます。get_scoped_configは関数ですが、full_configは属性であることに注意してください。

from botocore.session import Session

# prodaccessプロファイルのセッションを作成
# (profile引数を省略するとdefaultプロファイルを使用)
session = Session(profile='prodaccess')

# 指定したプロファイルの設定を取得
scoped_config = session.get_scoped_config()

# すべてのプロファイルの設定を取得
full_config = session.full_config

scoped_configは次のような辞書オブジェクトになっています。

{
  "region": "ap-northeast-1",
  "role_arn": "arn:aws:iam::123456789012:role/ProductionAccessRole",
  "source_profile": "default"
}

full_configは次のような辞書オブジェクトになっています。指定したプロファイルの設定だけではなく、すべてのプロファイルの設定が取得できています。

{
  "profiles": {
    "default": {
      "region": "ap-northeast-1",
      "aws_access_key_id": "AKIAIOSFODNN7EXAMPLE",
      "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    },
    "prodaccess": {
      "region": "ap-northeast-1",
      "role_arn": "arn:aws:iam::123456789012:role/ProductionAccessRole",
      "source_profile": "default"
    }
  }
}

独自項目の追加

credentials, configファイルには独自の項目を設定できます。

[profile custom]
region = ap-northeast-1
role_arn = arn:aws:iam::123456789012:role/ProductionAccessRole
source_profile = default
my_custom_parameter = custom value

独自に設定した項目も同じように取得できます。

{
  "region": "ap-northeast-1",
  "role_arn": "arn:aws:iam::123456789012:role/ProductionAccessRole",
  "source_profile": "default",
  "my_custom_parameter": "custom value"
}

AWSを利用した独自ツールの設定情報として使うとよいかもしれません。

もともとconfigファイルに設定できる既存の項目については次のページに記載されています。独自の項目を設定する場合には、既存の項目と同名にならないように気をつけたほうがよいでしょう。

AWS CLI Configuration Variables - AWS CLI Command Reference https://docs.aws.amazon.com/cli/latest/topic/config-vars.html

boto3を使った一時的なAWS認証情報の取得

概要

IAMロールの切り替えを利用している場合の一時的なAWS認証情報の取得方法について説明します。boto3を使うと、AWS CLIのプロファイル設定をもとに認証情報を簡単に取得することができます。

IAMロールの切り替え

AWS CLIでIAMロールの切り替えを行う場合は、以下のようなプロファイル設定をします。

[profile prodaccess]
role_arn = arn:aws:iam::123456789012:role/ProductionAccessRole
source_profile = default

設定内容の詳細については以下のページが参考になります。

IAM ロールの切り替え(AWS Command Line Interface) - AWS Identity and Access Management https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_use_switch-role-cli.html

また、IAMロールの切り替え自体については以下のページが参考になります。

チュートリアル: AWS アカウント間の IAM ロールを使用したアクセスの委任 - AWS Identity and Access Management https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html

boto3を使った一時的なAWS認証情報の取得

使用したバージョンは以下の通りです。 * Python 3.6.4 * boto3 1.5.21

boto3は明示的にIAMロールの切り替えを行わなくても、プロファイル設定を見て必要があれば自動的にIAMロールの切り替えを行ってくれます。そのため、以下のような簡単なPythonコードで一時的なAWS認証情報を取得できます。

import boto3

session = boto3.session.Session(profile_name='prodaccess')
credentials = session.get_credentials()

print('export AWS_ACCESS_KEY_ID={}'.format(credentials.access_key))
print('export AWS_SECRET_ACCESS_KEY={}'.format(credentials.secret_key))
print('export AWS_SESSION_TOKEN={}'.format(credentials.token))

このコードを実行すると以下のような出力が得られます。この出力をシェルで評価すると一時的なAWS認証情報として利用することができます。

$ python3 credentials.py
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_SESSION_TOKEN=AQoDYXdzEGcaEXAMPLE2gsYULo+Im5ZEXAMPLEeYjs1M2FUIgIJx9tQqNMBEXAMPLECvSRyh0FW7jEXAMPLEW+vE/7s1HRpXviG7b+qYf4nD00EXAMPLEmj4wxS04L/uZEXAMPLECihzFB5lTYLto9dyBgSDyEXAMPLEKEY9/g7QRUhZp4bqbEXAMPLENwGPyOj59pFA4lNKCIkVgkREXAMPLEjlzxQ7y52gekeVEXAMPLEDiB9ST3UusKdEXAMPLE1TVastU1A0SKFEXAMPLEiywCC/Cs8EXAMPLEpZgOs+6hz4AP4KEXAMPLERbASP+4eZScEXAMPLENhykxiHenDHq6ikBQ==

ただし、この方法では認証情報の有効時間を指定できないため、デフォルトの1時間が適用されます。

認証情報の有効時間(DurationSeconds)については以下に記載があります。

AssumeRole - AWS Security Token Service https://docs.aws.amazon.com/ja_jp/STS/latest/APIReference/API_AssumeRole.html