Ansibleで印刷♪

さてと、一旦自分のブログに書いたんですが、間違って記事どころかサイトデータ自体を rm -rf しちゃって絶望感がマックスむら…いや何でもないです。はい、書き直します。

えーと、何書いたっけかな…そうそう、鰻だ鰻。神様が「アドカレの 12/1 は鰻ネタを書いて布教せよ」って言うもんだから、鰻ネタ満載な記事にしてやろうと思ったんですよ。

でもね、ほら、僕 Ansible 界の人達とほとんど接点ないもんですから、うかつなことしてアドカレ出禁とかくらったらさすがにショックじゃないすか。なので大人しく鰻ネタじゃなく Ansible ネタを頑張って書いたんですよ。

まぁ、頑張って書いたところで結果は失敗ネタだったので、まぁ、いいんですけどね、いいんですが、同じ記事を2回も書くのは精神が削られるわけですよ。なので本当は「くすっ」っと笑ってもらえるような導入を書いたんですけどね、えぇ、はい、えぇ、消しちゃいましたよ、綺麗さっぱり。俺の鰻を。

で、どんな Ansible ネタを書いたかというと、まぁツマラナイもんですよ。「Ansible で物理スイッチや物理ストレージが設定/操作できるんだからプリンタもやれるべき」とか何とかそんな感じのことを。いやね、僕もできれば「第八宇宙から転生してきたチョネッパリイェルニャンダーがこの次元における歪みから第八宇宙に対して発せられているメロカンバーダシオン光線を止めるために強羅七億土天輝如来菩薩の協力のもと Ansible を使って七転八倒する」みたいな、面白すぎて少年少女の瞳孔がフルオープンするような記事を書きたかったんですよ。書きたかったんですが、ちょっとね、12/1 のエントリーから敷居をあげすぎると、 [OpenStack Advent Calendar 2016](https://adventar.org/calendars/1739) のときみたいに「次の日のエントリー、あれ僕だったんですが、すごくやり辛かったです」とか「余計に敷居があがった」とか「難易度高くするのやめて」とか「調子のりすぎ」「やりすぎ」「うなぎ」とか、褒めてんだか賞賛されてんだかよくわからないメッセージを貰うことになるので、自重しました。自重しすぎた結果、記事自体を rm で消しちゃうんですけどね。もうね、どんだけ自重しちゃってんの？って話。自重界のプリンスですよ、もうね。

---

さて、導入部がグダグダになったところで、Ansible のドキュメントにある [Developing Modules](http://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html) に従ってモジュールを書いてみました。

ドキュメントには「Should You Develop A Module?」なんて書いてありますが、僕は英語ができないので、全力スルーですよ。そんなこと言われたら「うん、確かに command モジュールで十分じゃん！」って思うのが人として当然なことなんですが、まぁ、ほら、前述したように、俺鰻ですからね。

で、書いたモジュールは以下です。これを ansible/lib/ansible/modules/printer/epson/epson_ep_804a.py って感じで配置しました。

```python
#!/usr/bin/python

ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'community'
}

DOCUMENTATION = '''
---
module: epson_ep_804a

short_description: This is printing module for EPSON EP-804A

version_added: "2.4"

description:
    - "This is printing module for EPSON EP-804A."

options:
    printer_name:
        description:
            - Prints files to the named printer.
        required: false
    copies:
        description:
            - Sets the number of copies to print.
        required: false
    files:
        description:
            - File name.
        required: true
    media_type:
        description:
            - "[Print Settings] Media Type"
        required: false
    print_quality:
        description:
            - "[Print Settings] Print Quality"
        required: false
    page_size:
        description:
            - "[Print Settings] Paper Size"
        required: false
    input_slot:
        description:
            - "[Print Settings] Paper Source"
        required: false
    color:
        description:
            - "[Print Settings] Color"
        required: false
    borderless:
        description:
            - "[Print Settings] Borderless"
        required: false
    output_paper:
        description:
            - "[Reduce/Enlarge Document] Output Paper"
        required: false
    reduce_enlarge:
        description:
            - "[Reduce/Enlarge Document] Reduce/Enlarge"
        required: false
    scale_ratio:
        description:
            - "[Reduce/Enlarge Document] Custom"
        required: false
    duplex:
        description:
            - "[Auto Duplex Printing] Duplex Printing - Binding Edge"
        required: false
    adjust_print_density:
        description:
            - "[Auto Duplex Printing] Adjust Print Density"
        required: false
    poster_printing:
        description:
            - "[Multi-Page] Poster"
        required: false
    rotate180:
        description:
            - "[Orientation] Rotate by 180 degrees"
        required: false
    mirror_image:
        description:
            - "[Orientation] Mirror Image"
        required: false
    cd_dvd_inner_print_position:
        description:
            - "[CD/DVD] CD/DVD Inner Print Position"
        required: false
    cd_dvd_print_pos_up_down:
        description:
            - "[CD/DVD] CD/DVD Print Pos. Up-Down"
        required: false
    cd_dvd_print_pos_left_right:
        description:
            - "[CD/DVD] CD/DVD Print Pos. Left-Right"
        required: false
    watermark:
        description:
            - "[Stampmark] Stampmark"
        required: false
    colur_watermark:
        description:
            - "[Stampmark] Color"
        required: false
    position_watermark:
        description:
            - "[Stampmark] Position"
        required: false
    density_watermark:
        description:
            - "[Stampmark] Density"
        required: false
    size_watermark:
        description:
            - "[Stampmark] Size"
        required: false
    correction_color:
        description:
            - "[Color Correction] Color Correction"
        required: false
    gamma_value:
        description:
            - "[Color Correction] Gamma"
        required: false
    brightness_value:
        description:
            - "[Color Correction] Brightness"
        required: false
    contrast_value:
        description:
            - "[Color Correction] Contrast"
        required: false
    saturation_value:
        description:
            - "[Color Correction] Saturation"
        required: false
    cyan_value:
        description:
            - "[Color Correction] Cyan"
        required: false

    magenta_value:
        description:
            - "[Color Correction] Magenta"
        required: false
    yellow_value:
        description:
            - "[Color Correction] Yellow"
        required: false

extends_documentation_fragment:
    - epson

author:
    - Osamu Habuka (@habuka036)
'''

EXAMPLES = '''
- name: Print two copies of a document to the default printer
  epson_ep_804a:
    copies: 2
    files: filename.txt

- name: Print a legal document to a printer called "foo":
  epson_ep_804a:
    printer_name: foo
    page_size: Legal
    files: filename.txt
'''

RETURN = '''
original_message:
    description: The original name param that was passed in
    type: str
message:
    description: The output message that the sample module generates
'''

import subprocess

from ansible.module_utils.basic import AnsibleModule

def run_module():
    module_args = dict(
        printer_name=dict(type='str', required=False),
        copies=dict(type='int', required=False),
        files=dict(type='list', required=True),
        media_type=dict(type='str', required=False),
        print_quality=dict(type='str', required=False),
        page_size=dict(type='str', required=False),
        input_slot=dict(type='str', required=False),
        color=dict(type='str', required=False),
        borderless=dict(type='str', required=False),
        output_paper=dict(type='str', required=False),
        reduce_enlarge=dict(type='str', required=False),
        scale_ratio=dict(type='str', required=False),
        duplex=dict(type='str', required=False),
        adjust_print_density=dict(type='str', required=False),
        poster_printing=dict(type='str', required=False),
        rotate180=dict(type='str', required=False),
        mirror_image=dict(type='str', required=False),
        cd_dvd_inner_print_position=dict(type='str', required=False),
        cd_dvd_print_pos_up_down=dict(type='str', required=False),
        cd_dvd_print_pos_left_right=dict(type='str', required=False),
        watermark=dict(type='str', required=False),
        colur_watermark=dict(type='str', required=False),
        position_watermark=dict(type='str', required=False),
        density_watermark=dict(type='str', required=False),
        size_watermark=dict(type='str', required=False),
        correction_color=dict(type='str', required=False),
        gamma_value=dict(type='str', required=False),
        brightness_value=dict(type='str', required=False),
        contrast_value=dict(type='str', required=False),
        saturation_value=dict(type='str', required=False),
        cyan_value=dict(type='str', required=False),
        magenta_value=dict(type='str', required=False),
        yellow_value=dict(type='str', required=False)
    )

    result = dict(
        cmdline='',
        exit_status=0,
        stderr='',
        stdout=''
    )

    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )    

    lpr_options = ['/usr/bin/lpr']

    if module.params['printer_name']:
        lpr_options.append("-P %s" % module.params['printer_name'])

    if module.params['copies']:
        lpr_options.append("-# %d" % module.params['copies'])

    if module.params['media_type']:
        lpr_options.append("-o MediaType=%s" % module.params['media_type'])

    if module.params['print_quality']:
        lpr_options.append("-o PrintQuality=%s" % module.params['print_quality'])

    if module.params['page_size']:
        lpr_options.append("-o PageSize=%s" % module.params['page_size'])

    if module.params['input_slot']:
        lpr_options.append("-o InputSlot=%s" % module.params['input_slot'])

    if module.params['color']:
        lpr_options.append("-o Color=%s" % module.params['color'])

    if module.params['borderless']:
        lpr_options.append("-o Borderless=%s" % module.params['borderless'])

    if module.params['output_paper']:
        lpr_options.append("-o OutputPaper=%s" % module.params['output_paper'])

    if module.params['reduce_enlarge']:
        lpr_options.append("-o ReduceEnlarge=%s" % module.params['reduce_enlarge'])

    if module.params['scale_ratio']:
        lpr_options.append("-o ScaleRatio=%s" % module.params['scale_ratio'])

    if module.params['duplex']:
        lpr_options.append("-o Duplex=%s" % module.params['duplex'])

    if module.params['adjust_print_density']:
        lpr_options.append("-o AdjustPrintDensity=%s" % module.params['adjust_print_density'])

    if module.params['poster_printing']:
        lpr_options.append("-o PosterPrinting=%s" % module.params['poster_printing'])

    if module.params['rotate180']:
        lpr_options.append("-o Rotate180=%s" % module.params['rotate180'])

    if module.params['mirror_image']:
        lpr_options.append("-o MirrorImage=%s" % module.params['mirror_image'])

    if module.params['cd_dvd_inner_print_position']:
        lpr_options.append("-o CDDVDInnerPrintPosition=%s" % module.params['cd_dvd_inner_print_position'])

    if module.params['cd_dvd_print_pos_up_down']:
        lpr_options.append("-o CDDVDPrintPosUpDown=%s" % module.params['cd_dvd_print_pos_up_down'])

    if module.params['cd_dvd_print_pos_left_right']:
        lpr_options.append("-o CDDVDPrintPosLeftRight=%s" % module.params['cd_dvd_print_pos_left_right'])

    if module.params['watermark']:
        lpr_options.append("-o Watermark=%s" % module.params['watermark'])

    if module.params['colur_watermark']:
        lpr_options.append("-o ColurWatermark=%s" % module.params['colur_watermark'])

    if module.params['position_watermark']:
        lpr_options.append("-o PositionWatermark=%s" % module.params['position_watermark'])

    if module.params['density_watermark']:
        lpr_options.append("-o DensityWatermark=%s" % module.params['density_watermark'])

    if module.params['size_watermark']:
        lpr_options.append("-o SizeWatermark=%s" % module.params['size_watermark'])

    if module.params['correction_color']:
        lpr_options.append("-o CorrectionColor=%s" % module.params['correction_color'])

    if module.params['gamma_value']:
        lpr_options.append("-o GammaValue=%s" % module.params['gamma_value'])

    if module.params['brightness_value']:
        lpr_options.append("-o BrightnessValue=%s" % module.params['brightness_value'])

    if module.params['contrast_value']:
        lpr_options.append("-o ContrastValue=%s" % module.params['contrast_value'])

    if module.params['saturation_value']:
        lpr_options.append("-o SaturationValue=%s" % module.params['saturation_value'])

    if module.params['cyan_value']:
        lpr_options.append("-o CyanValue=%s" % module.params['cyan_value'])

    if module.params['magenta_value']:
        lpr_options.append("-o MagentaValue=%s" % module.params['magenta_value'])

    if module.params['yellow_value']:
        lpr_options.append("-o YellowValue=%s" % module.params['yellow_value'])

    if module.params['files']:
        for f in module.params['files']:
            lpr_options.append(f)

    result['cmdline'] = " ".join(lpr_options)

    if module.check_mode:
        return result

    p = subprocess.Popen(lpr_options, stderr=subprocess.PIPE, stdout=subprocess.PIPE) 
    r = p.communicate()
    result['stdout'] = r[0]
    result['stderr'] = r[1]

    module.exit_json(**result)

def main():
    run_module()

if __name__ == '__main__':
    main()
```

いやー、消えたのがこっちのコードじゃなくてまだ良かった。Python書けないくせに頑張ってPythonらしからぬコードを書いたのに、これが消えたらもう今頃は鰻の歌を歌って YouTube にアップロードして YouTuber デビューをかましてましたからね。

で、次に以下の yaml を書きましたよ。これは unagy.yaml として。

```yaml
- name: print unagy logo
  connection: local
  hosts: localhost
  tasks:
    - name: printing
      epson_ep_804a:
        files: "{{ item }}"
        copies: 1
      with_items:
        - /home/osamu/鰻/unagi-logo.png
```

で、以下のように実行したんですよ。

```bash
$ ansible-playbook ./unagy.yaml 
 [WARNING]: Unable to parse /etc/ansible/hosts as an inventory source

 [WARNING]: No inventory was parsed, only implicit localhost is available

 [WARNING]: Could not match supplied host pattern, ignoring: all

 [WARNING]: provided hosts list is empty, only localhost is available

ERROR! no action detected in task. This often indicates a misspelled module name, or incorrect module path.

The error appears to have been in '/github.com/ansible/ansible/lib/ansible/modules/printer/epson/unagy.yaml': line 5, column 7, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

  tasks:
    - name: printing
      ^ here


The error appears to have been in '/github.com/ansible/ansible/lib/ansible/modules/printer/epson/unagy.yaml': line 5, column 7, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

  tasks:
    - name: printing
      ^ here
```

まぁ、あれですよ、元の記事だと「あるぇ〜？」「なんでぇ〜？」とか焦った様子を書いたんですが、もうね、消えちゃったあとだとこんなエラーごときじゃ焦らないわけですよ。もうね心はまるで南極の氷の下の100万年前の海ぐらい穏やかですよ。

まぁ、結局初日から失敗ネタで大変申し訳ないと思います。もうこんなに心踊らない記事を書いてる自分が腑甲斐ないやら何やらで、もう「なゆかどっかに失踪したい」と思ってしまうぐらい。( [@ishikawa84g](https://twitter.com/ishikawa84g) さん、「なゆか」ネタはもうこれで勘弁してください ><)

Ansible ネタなのにこんなしめくくりで申し訳ないんですが「git の管理下にあるものはとりあえず push しろ、ビルドだの何だのはあとでやれ」です。「人は rm を手に入れたときから破滅する未来も一緒に手に入れたのだ」とか「人類に rm -rf は早すぎた」とか、他にも色々と言えるんですが、蛇足なので割愛します。

---

さて、気を取り直して、明日の [Ansible Advent Calendar 2017](https://qiita.com/advent-calendar/2017/ansible) の二日目のエントリーは、日本鰻師同好会の名誉会員である [@tsukaman](https://qiita.com/tsukaman) さんの記事です。お楽しみに〜。