diff --git a/.travis.yml b/.travis.yml index 72f7f97..f139c43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,38 +1,43 @@ -# .travis.yml Execution script for role tests on Travis-CI --- +language: python + +# Use the new container infrastructure sudo: required env: + global: + - ROLE_NAME: samba matrix: - - DISTRIBUTION: centos - VERSION: 7 - - DISTRIBUTION: ubuntu - VERSION: 18.04 - - DISTRIBUTION: debian - VERSION: 9 - - DISTRIBUTION: fedora - VERSION: 29 + - MOLECULE_DISTRO: centos7 + - MOLECULE_DISTRO: debian9 + - MOLECULE_DISTRO: fedora29 + - MOLECULE_DISTRO: ubuntu1804 +#Enable docker support services: - docker -before_install: - # Install latest Git +install: - sudo apt-get update - - sudo apt-get install --only-upgrade git - - sudo apt-get install smbclient - # Allow fetching other branches than master - - git config remote.origin.fetch +refs/heads/*:refs/remotes/origin/* - # Fetch the branch with test code - - git fetch origin docker-tests - - git worktree add docker-tests origin/docker-tests + - sudo apt-get install bats curl smbclient samba-testsuite + # Install dependencies for Molecule test + - python3 -m pip install molecule yamllint ansible-lint docker + # Check ansible, molecule and nmblookup version + - ansible --version + - molecule --version + - nmblookup --version + # Create ansible.cfg with correct roles_path + - printf '[defaults]\nroles_path=../' >ansible.cfg + +before_script: + #Renames ansible-role-bind to bertvv.bind to make it match with Ansible Galaxy + - cd ../ + - mv ansible-role-$ROLE_NAME bertvv.$ROLE_NAME + - cd bertvv.$ROLE_NAME script: - # Create container and apply test playbook - - ./docker-tests/docker-tests.sh - - # Run functional tests on the container - - SUT_IP=172.17.0.2 ./docker-tests/functional-tests.sh + #Run molecule test + - molecule test notifications: - webhooks: https://galaxy.ansible.com/api/v1/notifications/ + webhooks: https://galaxy.ansible.com/api/v1/notifications/ \ No newline at end of file diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..8827676 --- /dev/null +++ b/.yamllint @@ -0,0 +1,33 @@ +--- +# Based on ansible-lint config +extends: default + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + colons: + max-spaces-after: -1 + level: error + commas: + max-spaces-after: -1 + level: error + comments: disable + comments-indentation: disable + document-start: disable + empty-lines: + max: 3 + level: error + hyphens: + level: error + indentation: disable + key-duplicates: enable + line-length: disable + new-line-at-end-of-file: disable + new-lines: + type: unix + trailing-spaces: disable + truthy: disable diff --git a/README.md b/README.md index cdcd389..e5ca7fc 100644 --- a/README.md +++ b/README.md @@ -211,10 +211,50 @@ See the [test playbook](https://github.com/bertvv/ansible-role-samba/blob/docker ## Testing -Two test environments are provided for this role: one set up with Vagrant, one with Docker. The Docker test environment is also used for the Travis-CI tests. Each test environment is stored in a separate orphan branch. See the README of each for details on how to set it up locally. +This role is tested using [Ansible Molecule](https://molecule.readthedocs.io/). Tests are launched automatically on [Travis CI](https://travis-ci.org/bertvv/ansible-role-samba) after each commit and PR. -- [Docker tests](https://github.com/bertvv/ansible-role-samba/blob/docker-tests/README.md) -- [Vagrant tests](https://github.com/bertvv/ansible-role-samba/blob/vagrant-tests/README.md) +This Molecule configuration will: + +- Run Yamllint and Ansible Lint +- Create a Docker container +- Run a syntax check +- Apply the role with a [test playbook](molecule/default/converge.yml) +- Run acceptance tests with [BATS](https://github.com/bats-core/bats-core/) + +This process is repeated for the supported Linux distributions. + +### Local test environment + +If you want to set up a local test environment, you can use this reproducible setup based on Vagrant+VirtualBox: . Steps to install the necessary tools manually: + +1. Docker, BATS and smbclient should be installed on your machine (assumed to run Linux). No Docker containers should be running when you start the test. +2. As recommended by Molecule, create a python virtual environment +3. Install the software tools `python3 -m pip install molecule docker yamllint ansible-lint` +4. Navigate to the root of the role directory and run `molecule test` + +Molecule automatically deletes the containers after a test. If you would like to check out the containers yourself, run `molecule converge` followed by `molecule login --host HOSTNAME`. + +The Docker containers are based on images created by [Jeff Geerling](https://hub.docker.com/u/geerlingguy), specifically for Ansible testing (look for images named `geerlingguy/docker-DISTRO-ansible`). You can use any of his images, but only the distributions mentioned in [meta/main.yml](meta/main.yml) are supported. + +The default config will start a Centos 7 container. Choose another distro by setting the `MOLECULE_DISTRO` variable with the command, e.g.: + +``` bash +MOLECULE_DISTRO=debian9 molecule test +``` + +or + +``` bash +MOLECULE_DISTRO=debian9 molecule converge +``` + +You can run the acceptance tests on both servers with `molecule verify` or manually with + +```console +SUT_IP=172.17.0.2 bats molecule/default/files/samba.bats +``` + +You need to initialise the variable `SUT_IP`, the system under test's IP address. The server, `smb1`, should have IP address 172.17.0.2. ## Contributing @@ -250,4 +290,5 @@ Pull requests are also very welcome. Please create a topic branch for your propo [Sven Eeckeman](https://github.com/SvenEeckeman), [Tiemo Kieft](https://github.com/blubber), [Tobias Wolter](https://github.com/towo), -[Tomohiko Ozawa](https://github.com/kota65535). +[Tomohiko Ozawa](https://github.com/kota65535), +[Robin Ophalvens](https://github.com/RobinOphalvens). diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100644 index 0000000..f454717 --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,93 @@ +--- +- name: Converge + hosts: all + pre_tasks: + - name: Ensure package database is up-to-date + apt: + update-cache: true + failed_when: false + changed_when: false + when: ansible_os_family == 'Debian' + - name: Create users + user: + name: "{{ item }}" + groups: users + append: true + with_items: + - usr1 + - usr2 + - timemachine + vars: + samba_netbios_name: SAMBA_TEST + samba_server_string: 'Welcome to the test file server' + samba_workgroup: TESTGROUP + samba_global_include: global-include.conf + samba_load_homes: true + samba_load_printers: false + samba_create_varwww_symlinks: true + samba_log: /var/log/samba.log + samba_log_size: 60000 + samba_log_level: '3 passdb:5 auth:10 winbind:2 ' + # The smbclient version of the Travis CI environment crashes when `min + # protocol' is set: + # protocol negotiation failed: NT_STATUS_INVALID_NETWORK_RESPONSE + # Uncomment the following lines if you want to test this setting locally. + # + # samba_server_min_protocol: SMB2 + # samba_server_max_protocol: SMB3 + samba_map_to_guest: Never + samba_users: + - name: usr1 + password: usr1 + - name: usr2 + password: usr2 + - name: timemachine + password: timemachine + samba_username_map: + - from: 'User Two' + to: usr2 + samba_shares_root: /srv/samba + samba_shares: + - name: restrictedshare + - name: privateshare + comment: 'Only readable/writeable by usr1' + valid_users: usr1 + write_list: usr1 + group: usr1 + browseable: 'no' + - name: protectedshare + public: 'yes' + comment: 'Public, but only writeable by usr2' + write_list: usr2 + group: users + browseable: 'yes' + include_file: protectedshare-include.conf + - name: publicshare + comment: 'Public share, writeable by all members of group ‘users’' + public: 'yes' + write_list: +users + group: users + setype: public_content_t + browseable: 'yes' + - name: guestshare + comment: 'Share accessible for guests' + guest_ok: 'yes' + writable: 'yes' + browseable: 'yes' + - name: TimeMachine + comment: 'Share useable as a TimeMachine backup target on MacOS' + vfs_objects: + - name: fruit + options: + - name: time machine + value: 'yes' + - name: streams_xattr + path: /srv/timemachine + write_list: timemachine + owner: timemachine + group: timemachine + public: 'no' + guest_ok: 'no' + browseable: 'no' + roles: + - role: bertvv.samba \ No newline at end of file diff --git a/molecule/default/files/functional-tests.sh b/molecule/default/files/functional-tests.sh new file mode 100755 index 0000000..7fe6ba9 --- /dev/null +++ b/molecule/default/files/functional-tests.sh @@ -0,0 +1,111 @@ +#! /usr/bin/env bash +# +# Author: Bert Van Vreckem +# +# Run BATS test files in the current directory, and the ones in the subdirectory +# matching the host name. +# +# The script installs BATS if needed. It's best to put ${bats_install_dir} in +# your .gitignore. + +set -o errexit # abort on nonzero exitstatus +set -o nounset # abort on unbound variable + +#{{{ Variables + +test_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +bats_archive="v1.1.0.tar.gz" +bats_url="https://github.com/bats-core/bats-core/archive/${bats_archive}" +bats_install_dir="/opt" +bats_default_location="${bats_install_dir}/bats/libexec/bats" +test_file_pattern="*.bats" + +# Color definitions +readonly reset='\e[0m' +readonly yellow='\e[0;33m' +readonly cyan='\e[0;36m' +#}}} + +main() { + + bats=$(find_bats_executable) + + if [ -z "${bats}" ]; then + install_bats + bats="${bats_default_location}" + fi + + debug "Using BATS executable at: ${bats}" + + # List all test cases (i.e. files in the test dir matching the test file + # pattern) + + # Tests to be run on all hosts + global_tests=$(find_tests "${test_dir}" 1) + + # Tests for individual hosts + host_tests=$(find_tests "${test_dir}/${HOSTNAME}") + + # Loop over test files + for test_case in ${global_tests} ${host_tests}; do + info "Running test ${test_case}" + ${bats} "${test_case}" + done +} + +#{{{ Functions + +# Tries to find BATS executable in the PATH or the place where this script +# installs it. +find_bats_executable() { + if which bats > /dev/null; then + which bats + elif [ -x "${bats_default_location}" ]; then + echo "${bats_default_location}" + else + echo "" + fi +} + +# Usage: install_bats +install_bats() { + pushd "${bats_install_dir}" > /dev/null 2>&1 + curl --location --remote-name "${bats_url}" + tar xzf "${bats_archive}" + mv bats-* bats + rm "${bats_archive}" + popd > /dev/null 2>&1 +} + +# Usage: find_tests DIR [MAX_DEPTH] +# +# Finds BATS test suites in the specified directory +find_tests() { + local max_depth="" + if [ "$#" -eq "2" ]; then + max_depth="-maxdepth $2" + fi + + local tests + tests=$(find "$1" ${max_depth} -type f -name "${test_file_pattern}" -printf '%p\n' 2> /dev/null) + + echo "${tests}" +} + +# Usage: info [ARG]... +# +# Prints all arguments on the standard output stream +info() { + printf "${yellow}### %s${reset}\n" "${*}" +} + +# Usage: debug [ARG]... +# +# Prints all arguments on the standard output stream +debug() { + printf "${cyan}### %s${reset}\n" "${*}" +} +#}}} + +main diff --git a/molecule/default/files/samba.bats b/molecule/default/files/samba.bats new file mode 100644 index 0000000..628067a --- /dev/null +++ b/molecule/default/files/samba.bats @@ -0,0 +1,208 @@ +#! /usr/bin/env bats +# +# Author: Bert Van Vreckem +# +# Test a Samba server + +sut_wins_name=SAMBA_TEST # NetBIOS name +workgroup=TESTGROUP # Workgroup + +# The name of a directory and file that will be created to test for +# write access (= random string) +test_dir=peghawJaup +test_file=Nocideicye + +# {{{Helper functions + +# Checks if a user has shell access to the system +# Usage: assert_can_login USER PASSWD +assert_can_login() { + echo $2 | su -c 'ls ${HOME}' - $1 +} + +# Checks that a user has NO shell access to the system +# Usage: assert_cannot_login USER +assert_cannot_login() { + run sudo su -c 'ls' - $1 + [ "0" -ne "${status}" ] +} + +# Check that the guest account has read access +# Usage: assert_guest_read SHARE +assert_guest_read() { + local share="${1}" + + run smbclient "//${SUT_IP}/${share}" \ + --user=% \ + --command='ls' + + echo "${output}" + + [ "${status}" -eq "0" ] +} + +# Check that a user has read acces to a share +# Usage: read_access SHARE USER PASSWORD +assert_read_access() { + local share="${1}" + local user="${2}" + local password="${3}" + + run smbclient "//${SUT_IP}/${share}" \ + --user=${user}%${password} \ + --command='ls' + + echo "${output}" + + [ "${status}" -eq "0" ] +} + +# Check that a user has NO read access to a share +# Usage: no_read_access SHARE USER PASSWORD +assert_no_read_access() { + local share="${1}" + local user="${2}" + local password="${3}" + + run smbclient "//${SUT_IP}/${share}" \ + --user=${user}%${password} \ + --command='ls' + + echo "${output}" + + [ "${status}" -eq "1" ] +} + +# Check that a user has write access to a share +# Usage: write_access SHARE USER PASSWORD +assert_write_access() { + local share="${1}" + local user="${2}" + local password="${3}" + + run smbclient "//${SUT_IP}/${share}" \ + --user=${user}%${password} \ + --command="mkdir ${test_dir};rmdir ${test_dir}" + + echo "${output}" + + # Output should NOT contain any error message. Checking on exit status is + # not reliable, it can be 0 when the command failed... + [ -z "$(echo ${output} | grep NT_STATUS_)" ] +} + +# Check that a user has NO write access to a share +# Usage: no_write_access SHARE USER PASSWORD +assert_no_write_access() { + local share="${1}" + local user="${2}" + local password="${3}" + + run smbclient "//${SUT_IP}/${share}" \ + --user=${user}%${password} \ + --command="mkdir ${test_dir};rmdir ${test_dir}" + + echo "${output}" + + # Output should contain an error message (beginning with NT_STATUS, usually + # NT_STATUS_MEDIA_WRITE_PROTECTED + [ -n "$(echo ${output} | grep NT_STATUS_)" ] +} + +# Check that users from the same group can write to each other’s files +# Usage: assert_group_write_file SHARE USER1 PASSWD1 USER2 PASSWD2 +assert_group_write_file() { + local share="${1}" + local user1="${2}" + local passwd1="${3}" + local user2="${4}" + local passwd2="${5}" + + echo "Hello world!" > ${test_file} + + smbclient "//${SUT_IP}/${share}" --user=${user1}%${passwd1} \ + --command="put ${test_file}" + # In order to overwrite the file, write access is needed. This will fail + # if user2 doesn’t have write access. + smbclient "//${SUT_IP}/${share}" --user=${user2}%${passwd2} \ + --command="put ${test_file}" +} + +# Check that users from the same group can write to each other’s directories +# Usage: assert_group_write_dir SHARE USER1 PASSWD1 USER2 PASSWD2 +assert_group_write_dir() { + local share="${1}" + local user1="${2}" + local passwd1="${3}" + local user2="${4}" + local passwd2="${5}" + + smbclient "//${SUT_IP}/${share}" --user=${user1}%${passwd1} \ + --command="mkdir ${test_dir}; mkdir ${test_dir}/tst" + run smbclient "//${SUT_IP}/${share}" --user=${user2}%${passwd2} \ + --command="rmdir ${test_dir}/tst" + [ -z $(echo "${output}" | grep NT_STATUS_ACCESS_DENIED) ] +} + +#}}} + +@test 'NetBIOS name resolution should work' { + #skip + # Look up the Samba server’s NetBIOS name under the specified workgroup + # The result should contain the IP followed by NetBIOS name + nmblookup -U ${SUT_IP} --workgroup ${workgroup} ${sut_wins_name} | grep "^${SUT_IP} ${sut_wins_name}" +} + +# Read / write access to shares + +@test 'read access for share ‘restrictedshare’' { + # Share User Password + assert_read_access restrictedshare usr1 usr1 + assert_read_access restrictedshare usr2 usr2 +} + +@test 'write access for share ‘restrictedshare’' { + # Share User Password + assert_no_write_access restrictedshare usr1 usr1 + assert_no_write_access restrictedshare usr2 usr2 +} + +@test 'read access for share ‘privateshare’' { + # Share User Password + assert_read_access privateshare usr1 usr1 + assert_no_read_access privateshare usr2 usr2 +} + +@test 'write access for share ‘privateshare’' { + # Share User Password + assert_write_access privateshare usr1 usr1 + assert_no_write_access privateshare usr2 usr2 +} + +@test 'read access for share ‘protectedshare’' { + # Share User Password + assert_read_access protectedshare usr1 usr1 + assert_read_access protectedshare usr2 usr2 +} + +@test 'write access for share ‘protectedshare’' { + # Share User Password + assert_no_write_access protectedshare usr1 usr1 + assert_write_access protectedshare usr2 usr2 +} + +@test 'read access for share ‘publicshare’' { + # Share User Password + assert_read_access publicshare usr1 usr1 + assert_read_access publicshare usr2 usr2 +} + +@test 'write access for share ‘publicshare’' { + # Share User Password + assert_write_access publicshare usr1 usr1 + assert_write_access publicshare usr2 usr2 +} + +@test 'Guest access in share ‘guestshare’' { + assert_guest_read guestshare +} diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100644 index 0000000..04a7998 --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,33 @@ +--- +dependency: + name: galaxy +driver: + #Specifies the driver that should be used. Podman should also work + name: docker +lint: | + yamllint . + ansible-lint --exclude=molecule/default/verify.yml +platforms: + #Set name and hostname + - name: smb1 + hostname: smb1 + #Specify which image should be used. Geerlingguys images are Ansible compatible and have Systemd installed + image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible:latest" + #Command to execute when the container starts + command: ${MOLECULE_DOCKER_COMMAND:-""} + #Volumes to mount within the container. Important to enable systemd + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + #Give extended privileges to the container. Necessary for Systemd to operate within the container. + # DO NOT use extended privileges in a production environment! + privileged: true + #Allocate pseudo-TTY + tty: True + environment: + container: docker + +provisioner: + name: ansible +#Runs the verify.yml playbook. Testinfra is also a supported method. Check the Molecule documention for more information. +verifier: + name: ansible diff --git a/molecule/default/templates/global-include.conf b/molecule/default/templates/global-include.conf new file mode 100644 index 0000000..bea63d9 --- /dev/null +++ b/molecule/default/templates/global-include.conf @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +# These come from Debians 4.8.4 config file +panic action = /usr/share/samba/panic-action %d +obey pam restrictions = yes +unix password sync = yes +passwd program = /usr/bin/passwd %u +passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* . +pam password change = yes + +# vim: ft=samba diff --git a/molecule/default/templates/protectedshare-include.conf b/molecule/default/templates/protectedshare-include.conf new file mode 100644 index 0000000..f831f98 --- /dev/null +++ b/molecule/default/templates/protectedshare-include.conf @@ -0,0 +1,6 @@ +# Additional Samba options for protectedshare +# {{ ansible_managed }} + +delete readonly = yes + +# vim: ft=samba diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml new file mode 100644 index 0000000..591cf27 --- /dev/null +++ b/molecule/default/verify.yml @@ -0,0 +1,9 @@ +--- +- name: Verify + hosts: all + tasks: + # We run the BATS tests from the localhost, since they are black box tests + - name: "Run BATS tests for {{ ansible_hostname }}" + shell: SUT_IP={{ ansible_default_ipv4.address }} bats {{ playbook_dir }}/files/samba.bats + delegate_to: localhost + changed_when: false \ No newline at end of file