├── tests ├── inventory └── test-simple-playbook.yml ├── tasks ├── ppd_ricoh.yml ├── ppd_hp.yml ├── cups_install.yml ├── cups_install_lpd.yml ├── ppd_install.yml ├── cups_install_ssl_cert.yml ├── main.yml ├── cups_cleanup.yml ├── cups_pre_install.yml ├── cups_configure.yml └── printer_and_class_install.yml ├── templates └── cups-lpd.j2 ├── meta └── main.yml ├── files └── hp-plugin-install.exp ├── LICENSE.txt ├── .travis.yml ├── defaults └── main.yml ├── README.md └── library └── cups_lpadmin.py /tests/inventory: -------------------------------------------------------------------------------- 1 | localhost -------------------------------------------------------------------------------- /tasks/ppd_ricoh.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install OpenPrinting Ricoh drivers 3 | apt: name=openprinting-ppds-postscript-ricoh state=latest 4 | 5 | - name: Extracting PPDs 6 | shell: find . -name '*.gz' -exec gzip --decompress --quiet {} \; 7 | args: 8 | chdir: "{{cups_ricoh_ppd_location}}" -------------------------------------------------------------------------------- /templates/cups-lpd.j2: -------------------------------------------------------------------------------- 1 | # CUPS-LPD XINET.D 2 | # {{ ansible_managed }} 3 | 4 | service printer 5 | { 6 | socket_type = stream 7 | protocol = tcp 8 | port = 515 9 | wait = no 10 | user = {{ cups_lpd_usn }} 11 | group = {{ cups_admin_grp }} 12 | server = /usr/lib/cups/daemon/cups-lpd 13 | server_args = -o document-format=application/octet-stream 14 | disable = no 15 | } -------------------------------------------------------------------------------- /tasks/ppd_hp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install HPLIP 3 | apt: name=hplip state=latest 4 | 5 | - name: Copy hp-plugin-install.exp install script to {{ cups_tmp_location }} 6 | copy: 7 | src: "files/hp-plugin-install.exp" 8 | dest: "{{cups_tmp_location}}/hp-plugin-install.exp" 9 | mode: a+rx 10 | 11 | - name: Installing HP Plugin using an except script to avoid user interaction 12 | command: "{{cups_tmp_location}}/hp-plugin-install.exp" -------------------------------------------------------------------------------- /tasks/cups_install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install CUPS 3 | apt: name={{ item }} state=latest 4 | with_items: 5 | - "{{cups_packages_to_install}}" 6 | 7 | - name: Add accounts to lpadmin group (CUPS admin) 8 | user: 9 | name: "{{item}}" 10 | append: yes 11 | groups: "{{cups_admin_grp}}" 12 | with_items: 13 | - "{{cups_lpadmin_users}}" 14 | 15 | - name: Include - CUPS-LPD 16 | include: cups_install_lpd.yml 17 | when: cups_lpd -------------------------------------------------------------------------------- /tasks/cups_install_lpd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install xinetd for cups-lpd 3 | apt: name=xinetd state=latest 4 | 5 | - name: Create cups-lpd user - {{ cups_lpd_usn }} 6 | user: 7 | name: "{{cups_lpd_usn}}" 8 | append: yes 9 | groups: "{{cups_admin_grp}}" 10 | 11 | - name: Copying over cups-lpd using cups-lpd.j2 template 12 | template: 13 | src: "cups-lpd.j2" 14 | dest: "{{cups_xinetd_location}}/cups-lpd" 15 | owner: root 16 | group: root 17 | mode: 0755 -------------------------------------------------------------------------------- /tasks/ppd_install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include - Install Ricoh PPDs. 3 | include: ppd_ricoh.yml 4 | when: cups_ricoh_openprinting_ppds 5 | 6 | - name: Include - HP PPDs - HPLIP. 7 | include: ppd_hp.yml 8 | when: cups_hplip 9 | 10 | - name: Copy PPDs in the ppds_to_be_copied folder 11 | copy: 12 | src: "{{cups_ppd_files_to_be_copied}}/" 13 | dest: "{{cups_ppd_shared_location}}/" 14 | owner: root 15 | group: root 16 | mode: 0644 17 | when: cups_ppd_files_to_be_copied|default("") != "" -------------------------------------------------------------------------------- /tests/test-simple-playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | remote_user: root 4 | 5 | vars: 6 | cups_printers_and_classes_to_be_removed: 7 | - TEST 8 | - Xerox 9 | 10 | cups_printer_list: 11 | - name: "TestPrinter1" 12 | uri: "file:///dev/null" 13 | - name: "TestPrinter2" 14 | uri: "file:///dev/null" 15 | 16 | cups_class_list: 17 | - name: "TestClass" 18 | members: 19 | - "TestPrinter1" 20 | - "TestPrinter2" 21 | 22 | roles: 23 | - ansible-cups -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Hitesh Prabhakar 4 | description: Installs CUPS, installs necessary PPDs and installs printers and classes on CUPS 5 | license: MIT 6 | min_ansible_version: 2.1 7 | platforms: 8 | - name: Debian 9 | versions: 10 | - stretch 11 | - jessie 12 | - wheezy 13 | - name: Ubuntu 14 | versions: 15 | - xenial 16 | - trusty 17 | - precise 18 | 19 | galaxy_tags: 20 | - cups 21 | - printing 22 | 23 | dependencies: [] 24 | -------------------------------------------------------------------------------- /tasks/cups_install_ssl_cert.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Copying the public key over to final destination 3 | copy: 4 | src: "{{cups_source_ssl_public_key_location}}" 5 | dest: "{{cups_etc_location}}/ssl/{{ansible_fqdn}}.crt" 6 | owner: root 7 | group: root 8 | mode: 0600 9 | remote_src: True 10 | 11 | - name: Copying the private key 12 | copy: 13 | src: "{{cups_source_ssl_private_key_location}}" 14 | dest: "{{cups_etc_location}}/ssl/{{ansible_fqdn}}.key" 15 | owner: root 16 | group: root 17 | mode: 0600 18 | remote_src: True -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - block: 3 | - name: Include - Pre-Install steps 4 | include: cups_pre_install.yml 5 | 6 | - name: Include - Install CUPS 7 | include: cups_install.yml 8 | 9 | - name: Include - Configure CUPS 10 | include: cups_configure.yml 11 | 12 | - name: Include - Install PPDs. 13 | include: ppd_install.yml 14 | 15 | - name: Include - Uninstall any defined printers and install any printers and classes defined. 16 | include: printer_and_class_install.yml 17 | 18 | always: 19 | - name: Include - CUPS Cleanup 20 | include: cups_cleanup.yml -------------------------------------------------------------------------------- /tasks/cups_cleanup.yml: -------------------------------------------------------------------------------- 1 | - block: 2 | - name: Delete {{ cups_tmp_location }} 3 | file: 4 | path: "{{cups_tmp_location}}" 5 | state: absent 6 | 7 | # As grep was used in the initial command whos output is registered as cups_papercut_expect_pkgs_already_installed. 8 | # The results dict will contain the results if the package already existed or not. If there output is none or the 9 | # command failed to find "Install ok installed" then it means the package wasn't installed beforehand and therfore 10 | # can be uninstalled after the precessing of this script. 11 | - name: Uninstall the expect pacakges if installed before 12 | apt: name={{ item.0 }} state=absent 13 | when: (item.1|failed) or (item.1.stdout == "") 14 | with_together: 15 | - "{{cups_expect_pkgs}}" 16 | - "{{cups_expect_pkgs_already_installed.results}}" 17 | 18 | ignore_errors: True -------------------------------------------------------------------------------- /files/hp-plugin-install.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | # Auto generated by autoexpect 4 | set force_conservative 0 ;# set to 1 to force conservative mode even if 5 | ;# script wasn't run conservatively originally 6 | if {$force_conservative} { 7 | set send_slow {1 .1} 8 | proc send {ignore arg} { 9 | sleep .1 10 | exp_send -s -- $arg 11 | } 12 | } 13 | 14 | set timeout 60 15 | 16 | # Spawning a shell and running the command 17 | spawn $env(SHELL) 18 | match_max 100000 19 | 20 | send -- "hp-plugin -i\r" 21 | 22 | # The different prompts that come up when installing HP Plugin and responding accordingly. 23 | expect "Do you wish to download and re-install the plug-in? (y=yes*, n=no, q=quit) ?" { send -- "y\r" } 24 | expect "Enter option (d=download*, p=specify path, q=quit) ?" { send -- "d\r" } 25 | expect "Do you accept the license terms for the plug-in (y=yes*, n=no, q=quit) ?" { send -- "y\r" } 26 | 27 | # Finally exiting the shell created above. 28 | expect ":" { send -- "exit\r" } 29 | expect eof -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Twitter, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python 3 | python: "2.7" 4 | sudo: required 5 | dist: precise 6 | 7 | matrix: 8 | include: 9 | - os: linux 10 | sudo: required 11 | dist: trusty 12 | 13 | env: 14 | - SITE=test-simple-playbook.yml 15 | 16 | before_install: 17 | - sudo apt-get -y -qq update 18 | - sudo apt-get -y -qq install aptitude 19 | 20 | install: 21 | # Install Ansible. 22 | - yes | pip install ansible 23 | 24 | # Add ansible.cfg to pick up roles path. 25 | - "{ echo '[defaults]'; echo 'roles_path = ../'; } >> ansible.cfg" 26 | 27 | script: 28 | # Check the role/playbook's syntax. 29 | - ansible-playbook -i tests/inventory tests/$SITE --syntax-check 30 | 31 | # Run the role/playbook with ansible-playbook. 32 | - script -q -c "ansible-playbook -i tests/inventory tests/$SITE --connection=local --sudo" output.txt 33 | 34 | - > 35 | grep -q 'failed=0' output.txt 36 | && (echo 'Role run: pass' && exit 0) 37 | || (echo 'Role run: fail' && exit 1) 38 | 39 | # Make sure CUPS is running. 40 | - > 41 | curl --insecure -s -o /dev/null -w "%{http_code}" http://localhost:631 42 | | grep -q '200' 43 | && (echo 'Status code 200 test: pass' && exit 0) 44 | || (echo 'Status code 200 test: fail' && exit 1) 45 | 46 | notifications: 47 | webhooks: https://galaxy.ansible.com/api/v1/notifications/ -------------------------------------------------------------------------------- /tasks/cups_pre_install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Creating CUPS tmp location {{ cups_tmp_location }} 3 | file: 4 | path: "{{cups_tmp_location}}" 5 | recurse: yes 6 | state: directory 7 | 8 | - name: Creating {{ cups_ppd_shared_location }} 9 | file: 10 | path: "{{cups_ppd_shared_location}}" 11 | recurse: yes 12 | state: directory 13 | 14 | - block: 15 | - name: Add OpenPrinting APT Key 16 | apt_key: 17 | id: "{{cups_openprinting_apt_key_id}}" 18 | keyserver: "{{cups_openprinting_key_server}}" 19 | 20 | - name: Add OpenPrinting Package repo 21 | apt_repository: 22 | repo: "{{cups_openprinting_repo}}" 23 | state: present 24 | update_cache: yes 25 | when: cups_openprinting_apt_required is defined and cups_openprinting_apt_required == True 26 | 27 | - name: Update apt cache. 28 | apt: 29 | update_cache: yes 30 | # upgrade: safe 31 | 32 | # If the output is none or the command failed to find "install ok installed" then it means the package wasn't installed beforehand 33 | - name: Check and register if expect related packages are already installed. 34 | command: dpkg -s {{item}} | grep 'install ok installed' 35 | register: cups_expect_pkgs_already_installed 36 | with_items: 37 | - "{{cups_expect_pkgs}}" 38 | changed_when: False 39 | failed_when: False 40 | 41 | - name: Ensure expect related packages are installed to guide us through CUPS installation. 42 | apt: name={{ item }} state=present 43 | with_items: 44 | - "{{cups_expect_pkgs}}" 45 | -------------------------------------------------------------------------------- /tasks/cups_configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Shutdown cups service(s) 3 | service: 4 | name: "{{item}}" 5 | state: stopped 6 | with_items: 7 | - "{{cups_services}}" 8 | 9 | - name: Copying over cupsd.conf using template if defined to {{cups_etc_location}}/cupsd.conf. 10 | template: 11 | src: "{{cups_cupsd_conf_template}}" 12 | dest: "{{cups_etc_location}}/cupsd.conf" 13 | owner: "{{cups_etc_files_perms_owner}}" 14 | group: "{{cups_etc_files_perms_grp}}" 15 | mode: "{{cups_etc_files_mode}}" 16 | when: cups_cupsd_conf_template|default("") != "" 17 | 18 | - name: Copying over cups-browsed.conf using template if defined to {{cups_etc_location}}/cups-browsed.conf. 19 | template: 20 | src: "{{cups_cups_browsed_conf_template}}" 21 | dest: "{{cups_etc_location}}/cups-browsed.conf" 22 | owner: "{{cups_etc_files_perms_owner}}" 23 | group: "{{cups_etc_files_perms_grp}}" 24 | mode: "{{cups_etc_files_mode}}" 25 | when: cups_cups_browsed_conf_template|default("") != "" 26 | 27 | - name: Copying over snmp.conf using template if defined to {{cups_etc_location}}/snmp.conf. 28 | template: 29 | src: "{{cups_snmp_conf_template}}" 30 | dest: "{{cups_etc_location}}/snmp.conf" 31 | owner: "{{cups_etc_files_perms_owner}}" 32 | group: "{{cups_etc_files_perms_grp}}" 33 | mode: "{{cups_etc_files_mode}}" 34 | when: cups_snmp_conf_template|default("") != "" 35 | 36 | - name: Include - Copy SSL certificates if necessary variables are defined - cups_source_ssl_private_key_location AND cups_source_ssl_public_key_location. 37 | include: cups_install_ssl_cert.yml 38 | when: (cups_source_ssl_private_key_location|default("") != "") and (cups_source_ssl_public_key_location|default("") != "") 39 | 40 | - name: Start back up cups service(s) 41 | service: 42 | name: "{{item}}" 43 | state: started 44 | with_items: 45 | - "{{cups_services}}" -------------------------------------------------------------------------------- /tasks/printer_and_class_install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Removing all printers and classes defined in cups_printers_printers_and_classes_to_be_removed. 3 | cups_lpadmin: 4 | name: "{{item}}" 5 | state: "absent" 6 | with_items: 7 | - "{{cups_printers_and_classes_to_be_removed}}" 8 | 9 | - name: Removing all printers and classes on server. 10 | cups_lpadmin: 11 | purge: True 12 | when: cups_purge_all_printers_and_classes 13 | 14 | - name: Install printers using cups_lpadmin 15 | cups_lpadmin: 16 | name: "{{item.name}}" 17 | printer_or_class: "printer" 18 | state: "{{item.state|default(cups_printer_default_state)}}" 19 | enabled: "{{item.enabled|default(cups_printer_default_enabled)}}" 20 | uri: "{{cups_printer_uri_prefix}}{{item.uri}}" 21 | default: "{{item.default_printer|default(omit)}}" 22 | model: "{{item.driver|default(omit)}}" 23 | location: "{{item.location|default(omit)}}" 24 | info: "{{item.info|default(omit)}}" 25 | report_ipp_supply_levels: "{{item.report_ipp_supply_levels|default(cups_printer_default_report_ipp_supplies)}}" 26 | report_snmp_supply_levels: "{{item.report_snmp_supply_levels|default(cups_printer_default_report_snmp_supplies)}}" 27 | shared: "{{item.shared|default(cups_printer_default_is_shared)}}" 28 | assign_cups_policy: "{{item.assign_cups_policy|default(cups_printer_default_assign_cups_policy)}}" 29 | job_kb_limit: "{{item.job_kb_limit|default(omit)}}" 30 | job_quota_limit: "{{item.job_quota_limit|default(omit)}}" 31 | job_page_limit: "{{item.job_page_limit|default(omit)}}" 32 | options: "{{item.options|default(omit)}}" 33 | with_items: 34 | - "{{cups_printer_list}}" 35 | 36 | - name: Create printer classes and assign printers to them 37 | cups_lpadmin: 38 | name: "{{item.name}}" 39 | printer_or_class: "class" 40 | state: "{{item.state|default(cups_class_default_state)}}" 41 | location: "{{item.location|default(omit)}}" 42 | info: "{{item.info|default(omit)}}" 43 | shared: "{{item.shared|default(cups_class_default_is_shared)}}" 44 | class_members: "{{item.members}}" 45 | with_items: 46 | - "{{cups_class_list}}" -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | cups_lpadmin_users: 3 | - root 4 | cups_lpd: True 5 | cups_lpd_usn: cupslpd 6 | 7 | cups_sysadmins_email: "sysadmins@{{ansible_fqdn}}" 8 | 9 | cups_cupsd_conf_template: "" 10 | cups_cups_browsed_conf_template: "" 11 | cups_snmp_conf_template: "" 12 | 13 | cups_hplip: True 14 | cups_ricoh_openprinting_ppds: True 15 | cups_openprinting_apt_required: "{{cups_ricoh_openprinting_ppds}}" 16 | 17 | cups_openprinting_apt_key_id: 24CBF5474CFD1E2F 18 | cups_openprinting_key_server: keyserver.ubuntu.com 19 | cups_openprinting_repo: "deb http://www.openprinting.org/download/printdriver/debian/ lsb3.2 main" 20 | 21 | cups_ppd_files_to_be_copied: "" 22 | 23 | cups__debops_ferm_dependent_rules: 24 | - name: 'cups_ipp_lpr_directip' 25 | chain: 'INPUT' 26 | type: 'accept' 27 | protocol: 'tcp' 28 | dport: ['631', '515', '9100'] 29 | accept_any: True 30 | 31 | cups_purge_all_printers_and_classes: False 32 | 33 | cups_printer_uri_prefix: "" 34 | 35 | cups_printer_default_state: "present" 36 | cups_printer_default_report_ipp_supplies: True 37 | cups_printer_default_report_snmp_supplies: True 38 | cups_printer_default_is_shared: True 39 | cups_printer_default_enabled: True 40 | cups_printer_default_assign_cups_policy: "default" 41 | 42 | cups_class_default_state: "present" 43 | cups_class_default_is_shared: True 44 | 45 | cups_printers_and_classes_to_be_removed: [] 46 | # - TEST 47 | # - Xerox 48 | 49 | cups_printer_list: [] 50 | # - name: "TestPrinter1" 51 | # printer_or_class: "printer" 52 | # state: "present" 53 | # uri: "file:///dev/null" 54 | # - name: "TestPrinter2" 55 | # printer_or_class: "printer" 56 | # state: "present" 57 | # uri: "file:///dev/null" 58 | 59 | cups_class_list: [] 60 | # - name: "TestClass" 61 | # state: "present" 62 | # class_members: 63 | # - "TestPrinter1" 64 | # - "TestPrinter2" 65 | 66 | cups_packages_to_install: 67 | - cups 68 | - cups-pdf 69 | cups_xinetd_location: "/etc/xinetd.d" 70 | cups_tmp_location: "/tmp/cups-ansible" 71 | cups_admin_grp: lpadmin 72 | cups_services: 73 | - cups 74 | cups_etc_location: "/etc/cups" 75 | cups_etc_files_perms_owner: "root" 76 | cups_etc_files_perms_grp: "lp" 77 | cups_etc_files_mode: 0644 78 | 79 | cups_expect_pkgs: 80 | - "expect" 81 | - "python-pexpect" 82 | cups_ppd_shared_location: "/opt/share/ppd" 83 | cups_ricoh_ppd_location: "/opt/OpenPrinting-Ricoh/ppds/Ricoh" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role: cups 2 | 3 | [![Build Status](https://travis-ci.org/HP41/ansible-cups.svg?branch=master)](https://travis-ci.org/HP41/ansible-cups) 4 | 5 | ## Installs CUPS, installs necessary PPDs and installs printers and classes on CUPS 6 | ### Install and configure CUPS 7 | * Installs `cups` and `cups-pdf` 8 | * Accounts defined in `cups_lpadmin_users` will be added to `lpadmin` group to administrate CUPS. 9 | * Installs `cups-lpd` if variables allow (see below): 10 | * Creates a user account which will run the cups-lpd process. 11 | * Installs `xinetd` to run cups-lpd as a service. Uses the cups-lpd template file to create the final xinetd config. 12 | * Configuring CUPS: 13 | * If templates for cupsd.conf, cups-browsed.conf and snmp.conf are provided they'll be built and copied over 14 | * If SSL certs are provided it'll copy them over to the proper location. 15 | 16 | ### Install PPDs 17 | * Creates `/opt/share/ppd` where CUPS looks for PPDs that are manually copied over. 18 | * Adds OpenPrinting Repo. 19 | * Install Ricoh OpenPrinting Package - `openprinting-ppds-postscript-ricoh` 20 | * Also unzip the PPDs it installs as the package installs them as gzip files in `/opt/OpenPrinting-Ricoh/ppds/Ricoh` 21 | * Installs HPLIP: 22 | * Also installs the HP proprietary plugin using an except script. 23 | * Copies over PPDs from the folder if specified in `cups_ppd_files_to_be_copied` to `/opt/share/ppd` 24 | 25 | ### Install Printers 26 | * Any printers defined to be removed will be removed first. 27 | * Install Printers listed in the `cups_printer_list` variable and then installs classes listed in the `cups_class_list` 28 | * See [cups_printer_list and cups_class_list](tasks/printer_install.yml) to see how to define each printer and class object in the variable `cups_printer_list` and `cups_class_list` respectively. 29 | * This uses the [cups_lpadmin](library/cups_lpadmin.py) module. There's documentation/comments within it on how it can be used. 30 | * cups\_lpadmin is a direct copy from [HP41.ansible-modules-extra](https://github.com/HP41/ansible-modules-extras)/system/cups\_lpadmin. Once it's merged upstream, it'll be removed from here. 31 | 32 | ## Requirements 33 | * Ansible >= 2.1 34 | * Guest machine: Debian 35 | - stretch 36 | - jessie 37 | - wheezy 38 | * Guest machine: Ubuntu 39 | - xenial 40 | - trusty 41 | - precise 42 | 43 | ## Possible additional tasks that are not part of this role's responsibilities. 44 | * Opening the necessary CUPS ports - 515(LPR), 631(IPP/IPPS), 9100(direct IP) through the firewall. 45 | * If you'd like to use [debops.ferm](https://github.com/debops/ansible-ferm) you can use/modify `cups__debops_ferm_dependent_rules` (defined in defaults) to pass through to [debops.ferm](https://github.com/debops/ansible-ferm). 46 | 47 | ## Default Variables that can be overridden or used as-is when using this role: 48 | ### CUPS install and config: 49 | * `cups_lpadmin_users`: List of users that must be added to cups admin (`lpadmin`) group. Default=root 50 | * `cups_lpd`: Whether to install and setup cups-lpd - Default=`True` 51 | * `cups_sysadmins_email`: The email that'll be used to build the cupsd.conf template - Default=`sysadmins@ansible_fqdn` 52 | * `cups__debops_ferm_dependent_rules`: Default simple rules to open up ports (515, 631, 9100) through firewall that can be referenced when using [debops.ferm](https://github.com/debops/ansible-ferm) role. 53 | * /etc/xinetd.d/cups-lpd 54 | * `cups_lpd_usn`: The username with which it'll run the cups-lpd process (through xinetd) - Default=`cupslpd` 55 | * Optional templates: 56 | * They could've been setup as a simple file copy but accessing and adding ansible variables into it will not be possible. With this ansible\_managed, ansible\_fqdn, etc are accessible. The templates could also be simple text files with no variable declaration and it'll get copied over. 57 | * `cups_cupsd_conf_template`: For /etc/cups/cupsd.conf 58 | * `cups_cups_browsed_conf_template`: For /etc/cups/cups-browsed.conf 59 | * `cups_snmp_conf_template`: For /etc/cups/snmp.conf 60 | 61 | ### Installation and copying of PPDs: 62 | * `cups_ppd_files_to_be_copied`: The folder to copy all .ppd files from - Default=None 63 | * `cups_hplip`: Should it install HPLIP - Default=`True` 64 | * `cups_ricoh_openprinting`: Should it install OpenPrinting-Ricoh drivers/PPDs - Default=`True` 65 | * `cups_openprinting_apt_required`: This is defined as a ternary. It controls if the OpenPrinting APT key and repo is added based on Ricoh drivers are being installed or not. It can be easily overriden to your value. 66 | * `cups_openprinting_apt_key_id`: The APT key id to obtain from keyserver below. Default=24CBF5474CFD1E2F 67 | * `cups_openprinting_key_server`: The keyserver to acquire the key from for the below repo - Default=keyserver.ubuntu.com 68 | * `cups_openprinting_repo`: The OpenPrinting Repo to add - Default="deb http://www.openprinting.org/download/printdriver/debian/ lsb3.2 main" 69 | 70 | 71 | ### Installation of Printers and classes: 72 | * `cups_printer_uri_prefix`: A URI prefix for any filters on top of the URI - Default="" 73 | * `cups_printer_report_ipp_supplies`: When printer object has no `report_ipp_supply_levels` attribute this value is used - Default=`True` 74 | * `cups_printer_report_snmp_supplies`: When printer object has no `report_snmp_supply_levels` attribute this value is used. - Default=`True` 75 | * `cups_printer_is_shared`: When printer object has no `shared` attribute this value is used - Default=`True` 76 | * `cups_class_is_shared`: When the class object has no `shared` attribute this value is used - Default=`True` 77 | * `cups_printer_list`: A **list** of hashes that contain printer information needed to install them. Please check [cups_lpadmin](library/cups_lpadmin.py) module and how [cups_printer_list](tasks/printer_install.yml) variable is used. 78 | * `cups_class_list`: A **list** of hashes that contain class information needed to install them. Please check [cups_lpadmin](library/cups_lpadmin.py) module and how [cups_class_list](tasks/printer_install.yml) variable is used. 79 | * `cups_purge_all_printers_and_classes`: Should the cups_lpadmin module purge/delete all printers before continuing. 80 | * `cups_printers_and_classes_to_be_removed`: Printers and classes you would like to specifically remove. 81 | 82 | ### Variables related to operation of the role and general CUPS setup: 83 | * `cups_packages_to_install`: The CUPS packages to install. This can be overridden for a specific package version if needed - Default=`cups, cups-pdf` 84 | * `cups_xinetd_location`: The location of xinet.d files - Default=`/etc/xinetd.d` 85 | * `cups_tmp_location`: Temp location that this role uses for copying files and running scripts. Location is created if it doesn't exist - Default=`/tmp/cups-ansible` 86 | * `cups_admin_grp`: The group that has admin access to CUPS. This is referenced when adding users (if defined) to CUPS admin roles - Default=`lpadmin` 87 | * `cups_services`: The CUPS service(s) that is referenced when starting and stopping CUPS service(s) for configuration purposes - Default=`cups` 88 | * `cups_etc_location`: etc location of CUPS config - Default=`/etc/cups` 89 | * `cups_etc_files_perms_owner`: Owner of files placed by this role under `cups_etc_location` - Default=`root` 90 | * `cups_etc_files_perms_grp`: Group membership of files placed by this role under `cups_etc_location` - Default=`lp` 91 | * `cups_etc_files_mode`: File mode of files placed by this role under `cups_etc_location` - Default=`0644` 92 | * `cups_expect_pkgs`: The expect related packages that are installed for unattended installations of different expect scripts within this role - Default=`expect, python-pexpect` 93 | * `cups_ppd_shared_location`: The standard shared location where PPDs can be placed and CUPS will pick them up - Default=`/opt/share/ppd` 94 | * `cups_ricoh_ppd_location`: The location where Ricoh PPDs from OpenPrinting are installed - Default=`/opt/OpenPrinting-Ricoh/ppds/Ricoh` -------------------------------------------------------------------------------- /library/cups_lpadmin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2015, David Symons (Multimac) 6 | (c) 2016, Konstantin Shalygin 7 | (c) 2016, Hitesh Prabhakar 8 | 9 | This file is part of Ansible 10 | 11 | This module is free software: you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation, either version 3 of the License, or 14 | (at your option) any later version. 15 | 16 | This software is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this software. If not, see . 23 | """ 24 | 25 | 26 | # =========================================== 27 | 28 | 29 | DOCUMENTATION = ''' 30 | --- 31 | module: cups_lpadmin 32 | author: 33 | - "David Symons (Multimac) " 34 | - "Konstantin Shalygin " 35 | - "Hitesh Prabhakar " 36 | short_description: Manages printers in CUPS printing system. 37 | description: 38 | - Creates, removes and sets options for printers in CUPS. 39 | - Creates, removes and sets options for classes in CUPS. 40 | - For class installation, the members are defined as a final state and therefore will only have the members defined. 41 | version_added: "2.1" 42 | notes: [] 43 | requirements: 44 | - CUPS 1.7+ 45 | options: 46 | name: 47 | description: 48 | - Name of the printer in CUPS. 49 | required: false 50 | default: null 51 | purge: 52 | description: 53 | - Task to purge all printers in CUPS. Convenient before deploy. 54 | required: false 55 | default: false 56 | choices: ["true", "false"] 57 | state: 58 | description: 59 | - Whether the printer should or not be in CUPS. 60 | required: false 61 | default: present 62 | choices: ["present", "absent"] 63 | printer_or_class: 64 | description: 65 | - State whether the object/item we are working on is a printer or class. 66 | required: false 67 | default: printer 68 | choices: ["printer", "class"] 69 | driver: 70 | description: 71 | - System V interface or PPD file. 72 | required: false 73 | default: model 74 | choices: ["model", "ppd"] 75 | uri: 76 | description: 77 | - The URI to use when connecting to the printer. This is only required in the present state. 78 | required: false 79 | default: null 80 | enabled: 81 | description: 82 | - Whether or not the printer should be enabled and accepting jobs. 83 | required: false 84 | default: true 85 | choices: ["true", "false"] 86 | shared: 87 | description: 88 | - Whether or not the printer should be shared on the network. 89 | required: false 90 | default: false 91 | choices: ["true", "false"] 92 | model: 93 | description: 94 | - The System V interface or PPD file to be used for the printer. 95 | required: false 96 | default: null 97 | default: 98 | description: 99 | - Set default server printer. Only one printer can be default. 100 | required: false 101 | default: false 102 | choices: ["true", "false"] 103 | info: 104 | description: 105 | - The textual description of the printer. 106 | required: false 107 | default: null 108 | location: 109 | description: 110 | - The textual location of the printer. 111 | required: false 112 | default: null 113 | assign_cups_policy: 114 | description: 115 | - Assign a policy defined in /etc/cups/cupsd.conf to this printer. 116 | required: false 117 | default: null 118 | class_members: 119 | description: 120 | - A list of printers to be added to this class. 121 | required: false 122 | default: [] 123 | type: list 124 | report_ipp_supply_levels: 125 | description: 126 | - Whether or not the printer must report supply status via IPP. 127 | required: false 128 | default: true 129 | choices: ["true", "false"] 130 | report_snmp_supply_levels: 131 | description: 132 | - Whether or not the printer must report supply status via SNMP (RFC 3805). 133 | required: false 134 | default: true 135 | choices: ["true", "false"] 136 | job_kb_limit: 137 | description: 138 | - Limit jobs to this printer (in KB) 139 | required: false 140 | default: null 141 | job_quota_limit: 142 | description: 143 | - Sets the accounting period for per-user quotas. The value is an integer number of seconds. 144 | required: false 145 | default: null 146 | job_page_limit: 147 | description: 148 | - Sets the page limit for per-user quotas. The value is the integer number of pages that can be printed. 149 | - Double sided pages are counted as 2. 150 | required: false 151 | default: null 152 | options: 153 | description: 154 | - A dictionary of key-value pairs describing printer options and their required value. 155 | default: {} 156 | required: false 157 | ''' 158 | 159 | # =========================================== 160 | 161 | 162 | EXAMPLES = ''' 163 | # Creates HP MFP via ethernet, set default A4 paper size and make this printer 164 | as server default. 165 | - cups_lpadmin: 166 | name: 'HP_M1536' 167 | state: 'present' 168 | printer_or_class: 'printer' 169 | uri: 'hp:/net/HP_LaserJet_M1536dnf_MFP?ip=192.168.1.2' 170 | model: 'drv:///hp/hpcups.drv/hp-laserjet_m1539dnf_mfp-pcl3.ppd' 171 | default: 'true' 172 | location: 'Room 404' 173 | info: 'MFP, but duplex broken, as usual on this model' 174 | printer_assign_policy: 'students' 175 | report_ipp_supply_levels: 'true' 176 | report_snmp_supply_levels: 'false' 177 | options: 178 | media: 'iso_a4_210x297mm' 179 | 180 | # Creates HP Printer via IPP (shared USB printer in another CUPS instance). 181 | Very important include 'snmp=false' to prevent adopt 'parent' driver, 182 | because if 'parent' receive not raw job this job have fail (filter failed). 183 | - cups_lpadmin: 184 | name: 'HP_P2055' 185 | state: 'present' 186 | uri: 'ipp://192.168.2.127:631/printers/HP_P2055?snmp=false' 187 | model: 'raw' 188 | options: 189 | media: 'iso_a4_210x297mm' 190 | 191 | # Create CUPS Class. 192 | - cups_lpadmin: 193 | name: 'StudentClass' 194 | state: 'present' 195 | printer_or_class: 'class' 196 | class_members: 197 | - CampusPrinter1 198 | - CampusPrinter2 199 | info: 'Printers for students' 200 | location: 'Room 404' 201 | 202 | # Deletes the printers/classes. 203 | - cups_lpadmin: 204 | name: 'HP_P2055' 205 | state: 'absent' 206 | printer_or_class: 'printer' 207 | - cups_lpadmin: 208 | name: 'StudentClass' 209 | state: 'absent' 210 | printer_or_class: 'class' 211 | 212 | # Purge all printers/classes. Useful when does not matter what we have now, 213 | client always receive new configuration. 214 | - cups_lpadmin: purge='true' 215 | ''' 216 | 217 | # =========================================== 218 | 219 | 220 | RETURN = ''' 221 | purge: 222 | description: Whether to purge all printers on CUPS or not. 223 | returned: when purge=True 224 | type: string 225 | sample: "True" 226 | state: 227 | description: The state as defined in the invocation of this script. 228 | returned: when purge=False 229 | type: string 230 | sample: "present" 231 | printer_or_class: 232 | description: Printer or Class as defined when this script was invoked. 233 | returned: when purge=False 234 | type: string 235 | sample: "class" 236 | name: 237 | description: The name of the destination (printer/class) as defined when the script was invoked. 238 | returned: when purge=False 239 | type: string 240 | sample: "Test-Printer" 241 | uri: 242 | description: The uri of the printer. 243 | returned: when purge=False and printer_or_class=printer 244 | type: string 245 | sample: "ipp://192.168.2.127:631/printers/HP_P2055?snmp=false" 246 | class_members: 247 | description: The members of the class. 248 | returned: when purge=False and printer_or_class=class 249 | type: string 250 | sample: "[TestPrinter1,TestPrinter2]" 251 | assign_cups_policy: 252 | description: The CUPS policy to assign this printer or class. 253 | returned: when purge=False and (printer_or_class=class or printer_or_class=printer) 254 | type: string 255 | sample: "[TestPrinter1,TestPrinter2]" 256 | changed: 257 | description: If any changes were made to the system when this script was run. 258 | returned: always 259 | type: boolean 260 | sample: "False" 261 | stdout: 262 | description: Output from all the commands run concatenated. Only returned if any changes to the system were run. 263 | returned: always 264 | type: string 265 | sample: "sample output" 266 | cmd_history: 267 | description: A concatenated string of all the commands run. 268 | returned: always 269 | type: string 270 | sample: "\nlpstat -p TEST \nlpinfo -l -m \nlpoptions -p TEST \nlpstat -p TEST \nlpstat -p TEST \nlpadmin -p TEST -o cupsIPPSupplies=true -o cupsSNMPSupplies=true \nlpoptions -p TEST -l " 271 | ''' 272 | 273 | 274 | # =========================================== 275 | 276 | 277 | class CUPSCommand(object): 278 | """ 279 | This is the main class that directly deals with the lpadmin command. 280 | 281 | Method naming methodology: 282 | - Methods prefixed with 'cups_item' or '_cups_item' can be used with both printer and classes. 283 | - Methods prefixed with 'class' or '_class' are meant to work with classes only. 284 | - Methods prefixed with 'printer' or '_printer' are meant to work with printers only. 285 | 286 | CUPSCommand handles printers like so: 287 | - If state=absent: 288 | - Printer exists: Deletes printer 289 | - Printer doesn't exist: Does nothing and exits 290 | - If state=present: 291 | - Printer exists: Checks printer options and compares them to the ones stated: 292 | - Options are different: Deletes the printer and installs it again with stated options. 293 | - Options are same: Does nothing and exits. 294 | - Printer doesn't exist: Installs printer with stated options. 295 | - Mandatory options are set every time if the right variables are defined. They are: 296 | - cupsIPPSupplies 297 | - cupsSNMPSupplies 298 | - printer-op-policy 299 | - job-k-limit 300 | - job-page-limit 301 | - job-quota-period 302 | 303 | CUPSCommand handles classes like so: 304 | - If state=absent: 305 | - Class exists: Deletes class 306 | - Class doesn't exist: Does nothing and exits 307 | - If state=present: 308 | - Class exists: Checks class options and members and compares them to the ones stated: 309 | - Options and members are different: Deletes the class and installs it again with 310 | stated options and stated members. 311 | - Options and members are same: Does nothing and exits. 312 | - Class doesn't exist: Installs class with stated options and members. 313 | - Mandatory options are set every time if the right variables are defined. They are: 314 | - cupsIPPSupplies 315 | - cupsSNMPSupplies 316 | - printer-op-policy 317 | - Notes about how classes are handled: 318 | - Members stated will be the final list of printers in that class. 319 | - It cannot add or remove printers from an existing list that might have more/other members defined. 320 | - It'll uninstall the class and create it from scratch as defined in this script if the defined member 321 | list and the actual member list don't match. 322 | """ 323 | 324 | def __init__(self, module): 325 | """ 326 | Assigns module vars to object. 327 | """ 328 | self.module = module 329 | 330 | self.driver = CUPSCommand.strip_whitespace(module.params['driver']) 331 | self.name = CUPSCommand.strip_whitespace(module.params['name']) 332 | self.printer_or_class = module.params['printer_or_class'] 333 | 334 | self.state = module.params['state'] 335 | self.purge = module.params['purge'] 336 | 337 | self.uri = CUPSCommand.strip_whitespace(module.params['uri']) 338 | 339 | self.enabled = module.params['enabled'] 340 | self.shared = module.params['shared'] 341 | self.default = module.params['default'] 342 | 343 | self.model = CUPSCommand.strip_whitespace(module.params['model']) 344 | 345 | self.info = CUPSCommand.strip_whitespace(module.params['info']) 346 | self.location = CUPSCommand.strip_whitespace(module.params['location']) 347 | 348 | self.options = module.params['options'] 349 | 350 | self.assign_cups_policy = CUPSCommand.strip_whitespace(module.params['assign_cups_policy']) 351 | 352 | self.class_members = module.params['class_members'] 353 | 354 | self.report_ipp_supply_levels = module.params['report_ipp_supply_levels'] 355 | self.report_snmp_supply_levels = module.params['report_snmp_supply_levels'] 356 | self.job_kb_limit = module.params['job_kb_limit'] 357 | self.job_quota_limit = module.params['job_quota_limit'] 358 | self.job_page_limit = module.params['job_page_limit'] 359 | 360 | self.out = "" 361 | self.cmd_history = "" 362 | self.changed = False 363 | 364 | self.cups_current_options = {} 365 | self.cups_expected_options = {} 366 | self.class_current_members = [] 367 | self.printer_current_options = {} 368 | 369 | self.check_mode = module.check_mode 370 | 371 | self.check_settings() 372 | 373 | def check_settings(self): 374 | """ 375 | Checks the values provided to the module and see if there are any missing/illegal settings. 376 | 377 | Module fails and exits if it encounters an illegal combination of variables sent to the module. 378 | :returns: None 379 | """ 380 | msgs = [] 381 | 382 | if self.state == 'printer': 383 | if not self.printer_or_class: 384 | msgs.append("When state=present printer or class must be defined.") 385 | 386 | if self.printer_or_class == 'printer': 387 | if not self.uri and not self.exists_self(): 388 | msgs.append("URI is required to install printer.") 389 | 390 | if self.printer_or_class == 'class': 391 | if not self.class_members and not self.exists_self(): 392 | self.module.fail_json(msg="Empty class cannot be created.") 393 | 394 | if msgs: 395 | "\n".join(msgs) 396 | self.module.fail_json(msg=msgs) 397 | 398 | @staticmethod 399 | def strip_whitespace(text): 400 | """ 401 | A static method to help with stripping white space around object variables. 402 | 403 | :returns: Trailing whitespace removed text or 'None' if input is 'None'. 404 | """ 405 | try: 406 | return text.strip() 407 | except: 408 | return None 409 | 410 | def append_cmd_out(self, cmd_out): 411 | """ 412 | Appends the out text from the command that was just run to the string with the out text of all the commands run. 413 | 414 | :param cmd_out: The text that was outputted during last command that was run. 415 | :returns: None 416 | """ 417 | if cmd_out: 418 | self.out = "{0}{1}{2}".format(self.out, "\n", cmd_out) 419 | 420 | def append_cmd_history(self, cmd): 421 | """ 422 | Appends the commands run into a single string. 423 | 424 | :param cmd: The command to be appended into the command history string. 425 | :returns: None 426 | """ 427 | safe_cmd = "" 428 | for x in cmd: 429 | x = str(x) 430 | if " " in x: 431 | if not ((x.startswith('"') and x.endswith('"')) or (x.startswith("'") and x.endswith("'"))): 432 | x = '{0}{1}{0}'.format('"', x) 433 | safe_cmd = "{0}{1}{2}".format(safe_cmd, x, " ") 434 | self.cmd_history = "{0}{1}{2}".format(self.cmd_history, "\n", safe_cmd) 435 | 436 | def _log_results(self, out): 437 | """ 438 | Method to log the details outputted from the command that was just run. 439 | 440 | :param out: Output text from the command that was just run. 441 | :returns: None 442 | """ 443 | self.append_cmd_out(out) 444 | 445 | def process_info_command(self, cmd): 446 | """ 447 | Runs a command that's meant to poll information only. 448 | 449 | Wraps around _process_command and ensures command output isn't logged as we're just fetching for information. 450 | 451 | :param cmd: The command to run. 452 | :returns: The output of _process_command which is return code, command output and error output. 453 | """ 454 | return self._process_command(cmd, log=False) 455 | 456 | def process_change_command(self, cmd, err_msg, only_log_on_error=False): 457 | """ 458 | Runs a command that's meant to change CUPS state/settings. 459 | 460 | Wraps around _process_command and ensures command output is logged as we're making changes to the system. 461 | An optional only_log_on_error is provided for the install_mandatory_options methods that are always run 462 | almost always and need not pollute the changed/output text with its information. This'll ensure the output 463 | and error text is only recorded when there's an error (err != None) and (rc != 0). 464 | 465 | It also is an easy way to centralize change command therefore making support_check_mode easier to implement. 466 | 467 | :param cmd: The command to run. 468 | :param err_msg: The error message with which to exit the module if an error occurred. 469 | :param only_log_on_error: The optional flag to record output if there's an error. Default=False 470 | :returns: The output of _process_command which is return code, command output and error output. 471 | """ 472 | (rc, out, err) = self._process_command(cmd, log=False) 473 | 474 | if rc != 0 and err: 475 | self.module.fail_json(msg="Error Message - {0}. Command Error Output - {1}.".format(err_msg, err)) 476 | 477 | if self.check_mode: 478 | self.module.exit_json(changed=True) 479 | 480 | if not only_log_on_error: 481 | self._log_results(out) 482 | self.changed = True 483 | 484 | return rc, out, err 485 | 486 | def _process_command(self, cmd, log=True): 487 | """ 488 | Runs a command given to it. Also logs the details if specified. 489 | 490 | :param cmd: The command to run. 491 | :param log: Boolean to specify if the command output should be logged. Default=True 492 | :returns: Return code, command output and error output of the command that was run. 493 | """ 494 | self.append_cmd_history(cmd) 495 | 496 | (rc, out, err) = self.module.run_command(cmd) 497 | 498 | if log: 499 | self._log_results(out) 500 | 501 | return rc, out, err 502 | 503 | def _printer_get_installed_drivers(self): 504 | """ 505 | Parses the output of lpinfo -l -m to provide a list of available drivers on machine. 506 | 507 | Example output from lpinfo -l -m: 508 | Model: name = gutenprint.5.2://xerox-wc_m118/expert 509 | natural_language = en 510 | make-and-model = Xerox WorkCentre M118 - CUPS+Gutenprint v5.2.11 511 | device-id = MFG:XEROX;MDL:WorkCentre M118;DES:XEROX WorkCentre M118; 512 | 513 | The output is parsed into a hash and then placed into the value of another hash where the key is the name field: 514 | 'gutenprint.5.2://xerox-wc_m118/expert': 'name': 'gutenprint.5.2://xerox-wc_m118/expert' 515 | 'natural_language': 'en' 516 | 'make-and-model': 'Xerox WorkCentre M118 - CUPS+Gutenprint v5.2.11' 517 | 'device-id': 'MFG:XEROX;MDL:WorkCentre M118;DES:XEROX WorkCentre M118;' 518 | 519 | :returns: Hash defining all the drivers installed on the system. 520 | """ 521 | cmd = ['lpinfo', '-l', '-m'] 522 | (rc, out, err) = self.process_info_command(cmd) 523 | 524 | # We want to split on sections starting with "Model:" as that specifies a new available driver 525 | prog = re.compile("^Model:", re.MULTILINE) 526 | cups_drivers = re.split(prog, out) 527 | 528 | drivers = {} 529 | for d in cups_drivers: 530 | 531 | # Skip if the line contains only whitespace 532 | if not d.strip(): 533 | continue 534 | 535 | curr = {} 536 | for l in d.splitlines(): 537 | kv = l.split('=', 1) 538 | 539 | # Strip out any excess whitespace from the key/value 540 | kv = tuple(map(str.strip, kv)) 541 | 542 | curr[kv[0]] = kv[1] 543 | 544 | # Store drivers by their 'name' (i.e. path to driver file) 545 | drivers[curr['name']] = curr 546 | 547 | return drivers 548 | 549 | def _printer_get_all_printers(self): 550 | """ 551 | Method to return all current printers and classes in CUPS. 552 | 553 | :returns: list of printer or classes names. 554 | """ 555 | cmd = ['lpstat', '-a'] 556 | (rc, out, err) = self.process_info_command(cmd) 557 | all_printers = [] 558 | 559 | if rc == 0: 560 | # Match only 1st column, where placed printer name 561 | all_printers = [line.split()[0] for line in out.splitlines()] 562 | 563 | return all_printers 564 | 565 | def cups_purge_all_items(self): 566 | """ 567 | Purge all printers and classes installed on CUPS. 568 | """ 569 | all_printers = self._printer_get_all_printers() 570 | 571 | for printer in all_printers: 572 | self.cups_item_uninstall(item_to_uninstall=printer) 573 | 574 | def _printer_get_make_and_model(self): 575 | """ 576 | Method to return the make and model of the driver/printer that is supplied to the object. 577 | 578 | If ppd is provided, ignore this as the ppd provided takes priority over finding a driver. 579 | 580 | If not ppd is provided (default behaviour), the model specified is used. 581 | It checks to see if the model specified is in the list of drivers installed on the system. If not, the whole 582 | module fails out with an error message. 583 | 584 | :returns: make-and-model of the model specified. 585 | """ 586 | if self.driver == 'model': 587 | # Raw printer is defined 588 | if not self.model or self.model == 'raw': 589 | return "Remote Printer" 590 | elif self.driver == 'ppd': 591 | return 592 | 593 | installed_drivers = self._printer_get_installed_drivers() 594 | 595 | if self.model in installed_drivers: 596 | return installed_drivers[self.model]['make-and-model'] 597 | 598 | self.module.fail_json(msg="Unable to determine printer make and model for printer '{0}'.".format(self.model)) 599 | 600 | def _printer_install(self): 601 | """ 602 | Installs the printer with the settings defined. 603 | """ 604 | cmd = ['lpadmin', '-p', self.name, '-v', self.uri] 605 | 606 | if self.enabled: 607 | cmd.append('-E') 608 | 609 | if self.shared: 610 | cmd.extend(['-o', 'printer-is-shared=true']) 611 | else: 612 | cmd.extend(['-o', 'printer-is-shared=false']) 613 | 614 | if self.model: 615 | if self.driver == 'model': 616 | cmd.extend(['-m', self.model]) 617 | elif self.driver == 'ppd': 618 | cmd.extend(['-P', self.model]) 619 | 620 | if self.info: 621 | cmd.extend(['-D', self.info]) 622 | 623 | if self.location: 624 | cmd.extend(['-L', self.location]) 625 | 626 | self.process_change_command(cmd, 627 | err_msg="Installing printer '{0}' failed" 628 | .format(self.name)) 629 | 630 | if self.default: 631 | cmd = ['lpadmin', '-d', self.name] 632 | self.process_change_command(cmd, 633 | err_msg="Setting printer '{0}' as default failed" 634 | .format(self.name)) 635 | 636 | def _printer_install_mandatory_options(self): 637 | """ 638 | Installs mandatory printer options. 639 | 640 | cupsIPPSupplies, cupsSNMPSupplies, job-k-limit, job-page-limit, printer-op-policy,job-quota-period 641 | cannot be checked via cups command-line tools yet. Therefore force set these options if they are defined. 642 | If there's an error running the command, the whole module will fail with an error message. 643 | """ 644 | orig_cmd = ['lpadmin', '-p', self.name] 645 | cmd = list(orig_cmd) # Making a copy of the list/array 646 | 647 | if self.report_ipp_supply_levels: 648 | cmd.extend(['-o', 'cupsIPPSupplies=true']) 649 | else: 650 | cmd.extend(['-o', 'cupsIPPSupplies=false']) 651 | 652 | if self.report_snmp_supply_levels: 653 | cmd.extend(['-o', 'cupsSNMPSupplies=true']) 654 | else: 655 | cmd.extend(['-o', 'cupsSNMPSupplies=false']) 656 | 657 | if self.job_kb_limit: 658 | cmd.extend(['-o', 'job-k-limit={0}'.format(self.job_kb_limit)]) 659 | 660 | if self.job_page_limit: 661 | cmd.extend(['-o', 'job-page-limit={0}'.format(self.job_page_limit)]) 662 | 663 | if self.job_quota_limit: 664 | cmd.extend(['-o', 'job-quota-period={0}'.format(self.job_quota_limit)]) 665 | 666 | if self.assign_cups_policy: 667 | cmd.extend(['-o', 'printer-op-policy={0}'.format(self.assign_cups_policy)]) 668 | 669 | if cmd != orig_cmd: 670 | self.process_change_command(cmd, 671 | err_msg="Install mandatory options for printer '{0}'" 672 | .format(self.name), 673 | only_log_on_error=True) 674 | 675 | def _printer_install_options(self): 676 | """ 677 | Installs any printer driver specific options defined. 678 | 679 | :returns: rc, out, err. The output of the lpadmin installation command. 680 | """ 681 | cmd = ['lpadmin', '-p', self.name] 682 | 683 | for k, v in self.options.iteritems(): 684 | cmd.extend(['-o', '{0}={1}'.format(k, v)]) 685 | 686 | if self.default: 687 | cmd.extend(['-d', self.name]) 688 | 689 | return self.process_change_command(cmd, 690 | err_msg="Install printer options for printer '{0}' failed".format(self.name)) 691 | 692 | def _class_install(self): 693 | """ 694 | Installs the class with the settings defined. 695 | 696 | It loops through the list of printers that are supposed to be in the class and confirms if they exists and 697 | adds them to the class. If any one of the printers don't exist, the whole module will fail with an error 698 | message. 699 | """ 700 | for printer in self.class_members: 701 | # Going through all the printers that are supposed to be in the class and adding them to said class 702 | # Ensuring first the printer exists 703 | if self.exists(item_to_check=printer): 704 | cmd = ['lpadmin', '-p', printer, '-c', self.name] 705 | self.process_change_command(cmd, 706 | err_msg="Failed to add printer '{0}' to class '{1}'" 707 | .format(printer, self.name)) 708 | else: 709 | self.module.fail_json(msg="Printer '{0}' doesn't exist and cannot be added to class '{1}'." 710 | .format(printer, self.name)) 711 | 712 | # Now that the printers are added to the class and the class created, we are setting up a few 713 | # settings for the class itself 714 | if self.exists_self(): 715 | cmd = ['lpadmin', '-p', self.name] 716 | 717 | if self.enabled: 718 | cmd.append('-E') 719 | 720 | if self.shared: 721 | cmd.extend(['-o', 'printer-is-shared=true']) 722 | else: 723 | cmd.extend(['-o', 'printer-is-shared=false']) 724 | 725 | if self.info: 726 | cmd.extend(['-D', self.info]) 727 | 728 | if self.location: 729 | cmd.extend(['-L', self.location]) 730 | 731 | self.process_change_command(cmd, 732 | err_msg="Failed to set Class options for class '{0}'" 733 | .format(self.name)) 734 | 735 | def _class_install_mandatory_options(self): 736 | """ 737 | Installs mandatory class options. 738 | 739 | cupsIPPSupplies, cupsSNMPSupplies, printer-op-policy,job-quota-period cannot be checked via 740 | cups command-line tools yet. Therefore force set these options if they are defined. 741 | If there's an error running the command, the whole module will fail with an error message. 742 | """ 743 | orig_cmd = ['lpadmin', '-p', self.name] 744 | cmd = list(orig_cmd) # Making a copy of the list/array 745 | 746 | if self.report_ipp_supply_levels: 747 | cmd.extend(['-o', 'cupsIPPSupplies=true']) 748 | else: 749 | cmd.extend(['-o', 'cupsIPPSupplies=false']) 750 | 751 | if self.report_snmp_supply_levels: 752 | cmd.extend(['-o', 'cupsSNMPSupplies=true']) 753 | else: 754 | cmd.extend(['-o', 'cupsSNMPSupplies=false']) 755 | 756 | if self.assign_cups_policy: 757 | cmd.extend(['-o', 'printer-op-policy={0}'.format(self.assign_cups_policy)]) 758 | 759 | if cmd != orig_cmd: 760 | self.process_change_command(cmd, 761 | err_msg="Installing mandatory options for class '{0}' failed" 762 | .format(self.name), 763 | only_log_on_error=True) 764 | 765 | def cups_item_uninstall_self(self): 766 | """ 767 | Uninstalls the printer or class defined in this class. 768 | """ 769 | self.cups_item_uninstall(item_to_uninstall=self.name) 770 | 771 | def cups_item_uninstall(self, item_to_uninstall): 772 | """ 773 | Uninstalls a printer or class given in item_to_uninstall if it exists else do nothing. 774 | 775 | :param item_to_uninstall: the CUPS Item (Printer or Class) that needs to be uninstalled. 776 | """ 777 | cmd = ['lpadmin', '-x'] 778 | 779 | if self.exists(item_to_check=item_to_uninstall): 780 | if item_to_uninstall: 781 | cmd.append(item_to_uninstall) 782 | self.process_change_command(cmd, 783 | err_msg="Uninstalling CUPS Item '{0}' failed" 784 | .format(item_to_uninstall)) 785 | else: 786 | self.module.fail_json(msg="Cannot delete/uninstall a cups item (printer/class) with no name.") 787 | 788 | def exists_self(self): 789 | """ 790 | Checks to see if the printer or class defined in this class exists. 791 | 792 | Using the lpstat command and based on if an error code is returned it can confirm if a printer or class exists. 793 | 794 | :returns: The return value of self.exists() 795 | """ 796 | return self.exists(item_to_check=self.name) 797 | 798 | def exists(self, item_to_check=None): 799 | """ 800 | Checks to see if a printer or class exists. 801 | 802 | Using the lpstat command and based on if an error code is returned it can confirm if a printer or class exists. 803 | 804 | :param item_to_check: The print or class name to check if it exists. 805 | 806 | :returns: True if return code form the command is 0 and therefore there where no errors and printer/class 807 | exists. Module exits if item_to_check is not defined. 808 | """ 809 | if item_to_check: 810 | cmd = ['lpstat', '-p', item_to_check] 811 | (rc, out, err) = self.process_info_command(cmd) 812 | return rc == 0 813 | else: 814 | self.module.fail_json(msg="Cannot check if a cups item (printer/class) exists that has no name.") 815 | 816 | def cups_item_get_cups_options(self): 817 | """ 818 | Returns a list of currently set options for the printer or class. 819 | 820 | Uses lpoptions -p command to list all the options, eg: 821 | copies=1 device-uri=socket://127.0.0.1:9100 finishings=3 job-cancel-after=10800 822 | job-hold-until=no-hold job-priority=50 job-sheets=none,none marker-change-time=0 number-up=1 823 | printer-commands=AutoConfigure,Clean,PrintSelfTestPage printer-info='HP LaserJet 4250 Printer Info' 824 | printer-is-accepting-jobs=true printer-is-shared=true printer-location=PrinterLocation 825 | printer-make-and-model='HP LaserJet 4250 Postscript (recommended)' printer-state=3 826 | printer-state-change-time=1463902120 printer-state-reasons=none printer-type=8425668 827 | printer-uri-supported=ipp://localhost/printers/TestPrinter 828 | 829 | :returns: A hash of the above info. 830 | """ 831 | cmd = ['lpoptions', '-p', self.name] 832 | (rc, out, err) = self.process_info_command(cmd) 833 | 834 | options = {} 835 | for s in shlex.split(out): 836 | kv = s.split('=', 1) 837 | 838 | if len(kv) == 1: # If we only have an option name, set it's value to None 839 | options[kv[0]] = None 840 | elif len(kv) == 2: # Otherwise set it's value to what we received 841 | options[kv[0]] = kv[1] 842 | 843 | self.cups_current_options = options 844 | 845 | return options 846 | 847 | def printer_check_cups_options(self): 848 | """ 849 | Creates a hash of the defined options sent to this module. 850 | Polls and retrieves a hash of options currently set for the printer. 851 | Compares them and returns True if the option values are satisfied or False if not satisfied. 852 | 853 | :returns: 'True' if the option values match else 'False'. 854 | """ 855 | expected_cups_options = { 856 | 'printer-make-and-model': self._printer_get_make_and_model(), 857 | 'printer-is-shared': 'true' if self.shared else 'false', 858 | } 859 | 860 | if self.info: 861 | expected_cups_options['printer-info'] = self.info 862 | if self.uri: 863 | expected_cups_options['device-uri'] = self.uri 864 | if self.location: 865 | expected_cups_options['printer-location'] = self.location 866 | 867 | self.cups_expected_options = expected_cups_options 868 | 869 | cups_options = self.cups_item_get_cups_options() 870 | 871 | # Comparing expected options as stated above to the options of the actual printer object. 872 | for k in expected_cups_options: 873 | if k not in cups_options: 874 | return False 875 | 876 | if expected_cups_options[k] != cups_options[k]: 877 | return False 878 | 879 | return True 880 | 881 | def class_check_cups_options(self): 882 | """ 883 | Creates a hash of the defined options sent to this module. 884 | Polls and retrieves a hash of options currently set for the class. 885 | Compares them and returns True if the option values are satisfied or False if not satisfied. 886 | 887 | :returns: 'True' if the option values match else 'False'. 888 | """ 889 | expected_cups_options = { 890 | 'printer-location': self.location, 891 | } 892 | 893 | if self.info: 894 | expected_cups_options['printer-info'] = self.info 895 | if self.location: 896 | expected_cups_options['printer-location'] = self.location 897 | 898 | self.cups_expected_options = expected_cups_options 899 | 900 | options = self.cups_item_get_cups_options() 901 | options_status = True 902 | 903 | # Comparing expected options as stated above to the options of the actual class object 904 | for k in expected_cups_options: 905 | if k not in options: 906 | options_status = False 907 | break 908 | 909 | if expected_cups_options[k] != options[k]: 910 | options_status = False 911 | break 912 | 913 | # Comparing expected class members and actual class members 914 | class_members_status = sorted(self.class_members) == sorted(self.class_get_current_members()) 915 | 916 | return options_status and class_members_status 917 | 918 | def class_get_current_members(self): 919 | """ 920 | Uses the lpstat -c command to get a list of members, eg: 921 | members of class TestClass: 922 | TestPrinter1 923 | TestPrinter2 924 | 925 | This is parsed into a list. The first line is skipped. 926 | 927 | :returns: A list of members for class specified in the module. 928 | """ 929 | cmd = ['lpstat', '-c', self.name] 930 | (rc, out, err) = self.process_info_command(cmd) 931 | 932 | if rc != 0: 933 | self.module.fail_json( 934 | msg="Error occurred while trying to discern class '{0}' members.".format(self.name)) 935 | 936 | members = [] 937 | # Skip first line as it's an information line, it end with a ':' 938 | (info, out) = out.split(':', 1) 939 | out = shlex.split(out) 940 | for m in out: 941 | str.strip(m) 942 | members.append(m) 943 | 944 | self.class_current_members = members 945 | 946 | return members 947 | 948 | def printer_get_specific_options(self): 949 | """ 950 | Returns a hash of printer specific options with its current value, available values and its label. 951 | Runs lpoptions -p -l, eg: 952 | HPCollateSupported/Collation in Printer: True288 *False288 953 | HPOption_500_Sheet_Feeder_Tray3/Tray 3: *True False 954 | HPOption_Duplexer/Duplex Unit: *True False 955 | HPOption_Disk/Printer Disk: True *False 956 | HPOption_PaperPolicy/Paper Matching: *Prompt Scale Crop 957 | HPServicesWeb/Services on the Web: *SupportAndTroubleshooting ProductManuals ColorPrintingAccessUsage 958 | OrderSupplies ShowMeHow 959 | HPServicesUtility/Device Maintenance: *DeviceAndSuppliesStatus 960 | Resolution/Printer Resolution: *600dpi 1200dpi 961 | PageSize/Page Size: *Letter Legal Executive HalfLetter w612h936 4x6 5x7 5x8 A4 A5 A6 RA4 B5 B6 W283H425 962 | w553h765 w522h737 w558h774 DoublePostcard Postcard Env10 Env9 EnvMonarch EnvISOB5 EnvC5 EnvC6 EnvDL 963 | Custom.WIDTHxHEIGHT 964 | InputSlot/Paper Source: *Auto Tray1 Tray2 Tray3 Tray1_Man 965 | Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble 966 | Collate/Collate: True *False 967 | 968 | This is parsed into a hash with option name as key and value with currently selected option, 969 | label of the option and available values eg: 970 | 'HPCollateSupported': 'current': 'False288' 971 | 'label': 'Collation in Printer' 972 | 'values': 'True288' 973 | 'False288' 974 | 975 | :returns: A hash of printer options. It includes currently set option and other available options. 976 | """ 977 | cmd = ['lpoptions', '-p', self.name, '-l'] 978 | (rc, out, err) = self.process_info_command(cmd) 979 | 980 | options = {} 981 | for l in out.splitlines(): 982 | remaining = l 983 | 984 | (name, remaining) = remaining.split('/', 1) 985 | (label, remaining) = remaining.split(':', 1) 986 | 987 | values = shlex.split(remaining) 988 | 989 | current_value = None 990 | for v in values: 991 | # Current value is prepended with a '*' 992 | if not v.startswith('*'): 993 | continue 994 | 995 | v = v[1:] # Strip the '*' from the value 996 | 997 | current_value = v 998 | break 999 | 1000 | options[name] = { 1001 | 'current': current_value, 1002 | 'label': label, 1003 | 'values': values, 1004 | } 1005 | 1006 | self.printer_current_options = options 1007 | 1008 | return options 1009 | 1010 | def printer_check_options(self): 1011 | """ 1012 | Returns if the defined options is the same as the options currently set for the printer. 1013 | :returns: Returns if the defined options is the same as the options currently set for the printer. 1014 | """ 1015 | expected_printer_options = self.options 1016 | 1017 | printer_options = self.printer_get_specific_options() 1018 | for k in expected_printer_options: 1019 | if k not in printer_options: 1020 | return False 1021 | 1022 | if expected_printer_options[k] != printer_options[k]['current']: 1023 | return False 1024 | 1025 | return True 1026 | 1027 | def printer_install(self): 1028 | """ 1029 | The main method that's called when state is 'present' and printer_or_class is 'printer'. 1030 | 1031 | It checks to see if printer exists and if its settings are the same as defined. 1032 | If not, it deletes it. 1033 | 1034 | It then checks to see if it exists again and installs it with defined settings if it doesn't exist. 1035 | 1036 | It also installs mandatory settings. 1037 | 1038 | Lastly it sets the printer specific options to the printer if it isn't the same. 1039 | """ 1040 | if self.exists_self() and not self.printer_check_cups_options(): 1041 | self.cups_item_uninstall_self() 1042 | 1043 | if not self.exists_self(): 1044 | self._printer_install() 1045 | 1046 | # cupsIPPSupplies, cupsSNMPSupplies, job-k-limit, job-page-limit, printer-op-policy, 1047 | # job-quota-period cannot be checked via cups command-line tools yet 1048 | # Therefore force set these options if they exist 1049 | if self.exists_self(): 1050 | self._printer_install_mandatory_options() 1051 | 1052 | if not self.printer_check_options(): 1053 | self._printer_install_options() 1054 | 1055 | def class_install(self): 1056 | """ 1057 | The main method that's called when state is 'present' and printer_or_class is 'class'. 1058 | 1059 | It checks to see if class exists and if its settings are the same as defined. 1060 | If not, it deletes it. 1061 | 1062 | It then checks to see if it exists again and installs it with defined settings if it doesn't exist. 1063 | 1064 | It also installs mandatory settings. 1065 | """ 1066 | if self.exists_self() and not self.class_check_cups_options(): 1067 | self.cups_item_uninstall_self() 1068 | 1069 | if not self.exists_self(): 1070 | self._class_install() 1071 | 1072 | if self.exists_self(): 1073 | self._class_install_mandatory_options() 1074 | 1075 | def start_process(self): 1076 | """ 1077 | This starts the process of processing the information provided to the module. 1078 | 1079 | Based on state, the following is done: 1080 | - state=present: 1081 | - printer_or_class=printer: 1082 | - Call CUPSCommand.printer_install() to install the printer. 1083 | - printer_or_class=class: 1084 | - Call CUPSCommand.class_install() to install the class. 1085 | - state=absent: 1086 | - Call CUPSCommand.cups_item_uninstall() to uninstall either a printer or a class. 1087 | 1088 | :returns: 'result' a hash containing the desired state. 1089 | """ 1090 | result = {} 1091 | 1092 | if self.purge: 1093 | self.cups_purge_all_items() 1094 | result['purge'] = self.purge 1095 | 1096 | else: 1097 | result['state'] = self.state 1098 | result['printer_or_class'] = self.printer_or_class 1099 | result['assign_cups_policy'] = self.assign_cups_policy 1100 | result['name'] = self.name 1101 | 1102 | if self.printer_or_class == 'printer': 1103 | if self.state == 'present': 1104 | self.printer_install() 1105 | else: 1106 | self.cups_item_uninstall_self() 1107 | result['uri'] = self.uri 1108 | 1109 | else: 1110 | if self.state == 'present': 1111 | self.class_install() 1112 | else: 1113 | self.cups_item_uninstall_self() 1114 | result['class_members'] = self.class_members 1115 | 1116 | result['changed'] = self.changed 1117 | 1118 | if self.out: 1119 | result['stdout'] = self.out 1120 | 1121 | # Verbose Logging info 1122 | if self.cmd_history: 1123 | result['cmd_history'] = self.cmd_history 1124 | if self.cups_current_options: 1125 | result['cups_current_options'] = self.cups_current_options 1126 | if self.cups_expected_options: 1127 | result['cups_expected_options'] = self.cups_expected_options 1128 | if self.class_current_members: 1129 | result['class_current_members'] = self.class_current_members 1130 | if self.printer_current_options: 1131 | result['printer_current_options'] = self.printer_current_options 1132 | 1133 | return result 1134 | 1135 | 1136 | # =========================================== 1137 | 1138 | 1139 | def main(): 1140 | """ 1141 | main function that populates this Ansible module with variables and sets it in motion. 1142 | 1143 | First an Ansible Module is defined with the variable definitions and default values. 1144 | Then a CUPSCommand is created using using this module. CUPSCommand populates its own values with the module vars. 1145 | 1146 | This CUPSCommand's start_process() method is called to begin processing the information provided to the module. 1147 | 1148 | Records the rc, out, err values of the commands run above and accordingly exists the module and sends the status 1149 | back to to Ansible using module.exit_json(). 1150 | """ 1151 | module = AnsibleModule( 1152 | argument_spec=dict( 1153 | state=dict(required=False, default='present', choices=['present', 'absent'], type='str'), 1154 | driver=dict(required=False, default='model', choices=['model', 'ppd'], type='str'), 1155 | purge=dict(required=False, default=False, type='bool'), 1156 | name=dict(required=False, type='str'), 1157 | printer_or_class=dict(default='printer', required=False, type='str', choices=['printer', 'class']), 1158 | uri=dict(required=False, default=None, type='str'), 1159 | enabled=dict(required=False, default=True, type='bool'), 1160 | shared=dict(required=False, default=False, type='bool'), 1161 | default=dict(required=False, default=False, type='bool'), 1162 | model=dict(required=False, default=None, type='str'), 1163 | info=dict(required=False, default=None, type='str'), 1164 | location=dict(required=False, default=None, type='str'), 1165 | assign_cups_policy=dict(required=False, default=None, type='str'), 1166 | class_members=dict(required=False, default=[], type='list'), 1167 | report_ipp_supply_levels=dict(required=False, default=True, type='bool'), 1168 | report_snmp_supply_levels=dict(required=False, default=True, type='bool'), 1169 | job_kb_limit=dict(required=False, default=None, type='int'), 1170 | job_quota_limit=dict(required=False, default=None, type='int'), 1171 | job_page_limit=dict(required=False, default=None, type='int'), 1172 | options=dict(required=False, default={}, type='dict'), 1173 | ), 1174 | supports_check_mode=True, 1175 | required_one_of=[['name', 'purge']], 1176 | mutually_exclusive=[['name', 'purge']] 1177 | ) 1178 | 1179 | cups_command = CUPSCommand(module) 1180 | result_info = cups_command.start_process() 1181 | module.exit_json(**result_info) 1182 | 1183 | # Import statements at the bottom as per Ansible best practices. 1184 | from ansible.module_utils.basic import * 1185 | 1186 | if __name__ == '__main__': 1187 | main() 1188 | --------------------------------------------------------------------------------