diff --git a/provision-contest/ansible/EOC/README.md b/provision-contest/ansible/EOC/README.md new file mode 100644 index 00000000..036142c0 --- /dev/null +++ b/provision-contest/ansible/EOC/README.md @@ -0,0 +1,81 @@ +## DOMjudge EOC ansible helper +This ansible playbook should automate most of the tasks DOMjudge needs to perform for the End Of Contest procedures during the ICPC WF's. It was created for the WF 2021 in Dhaka, which took place in 2022. + +Two types of authentication are used and supported. Session based auth and basic auth. + - Session based auth retrieves a CSRF token and fakes a login action. + - Basic auth headers are constructed and used. This is to fully mitigate [annoying basic-auth issues](https://askubuntu.com/questions/1070838/why-wget-does-not-use-username-and-password-in-url-first-time/1070847#1070847), even though the `force_basic_auth` option should already prevent these issues from arising. + +The full playbook can be run using the following command. See the setup section to see what the contents of facts.yml are supposed to be. + +```ansible-playbook main.yml --extra-vars "@facts.yml" --tags all``` + +### Steps automated. +The following steps from the EOC 2022-Oct document have been automated: + +29. `Primary pushes results.tsv into git repo`. Tag for just this action: `results.tsv`. Note, a manual commit is still needed! +32. `Compare scoreboard.json between primary and shadow`. Tag for just this action: `scoreboard`. Note, only if other (shadow or primary) credentials are provided. +33. `Compare awards.json between primary and CDS`. Tag for just this action: `awards`. Note, only if CDS credentials are provided. +36. `Verify that Primary and ... System Test`. Tag for just this action: `fetch`. Note, a manual commit is still needed. The static scoreboard (zip) and an export of the clarifications is also pulled from DOMjudge. + + +## Setup +To run this playbook a facts-file is required. An example of which can be seen below. Facts prefixed with: + - `dj_` are for interaction with a DOMjudge instance. + - `other_` are for interaction with another CLICS spec compliant CCS (commonly shadow if DOMjudge is primary, or vica versa) + - `cds_` are for interaction with the CDS (also CLICS compliant). + +Only the `repo`, `contest`, `contest_id`, and `dj_*` variables are required. When the `other_` or `cds_` facts are missing, steps which would interact with these systems are skipped. + +- `repo` must point to where all retrieved files should be stored, not simply the repo root! +- `contest` must be the externalID. +- `contest_id` must be the internalID. (cID) +- `jd_loc` must point to the location of a [`jd` binary](https://github.com/josephburnett/jd/releases/tag/v1.6.1). Used for comparing diffs. +- `contestutil_loc` must point to the location of [`contestUtil.sh`](https://github.com/icpctools/icpctools/releases). Used for comparing diffs. + +```yaml +repo: /home/mart/icpc/wf_repo +contest: bapc2022 +contest_id: 2 +jd_loc: /home/mart/icpc/EOC/jd +contestutil_loc: /home/mart/Downloads/contestUtil/contestUtil.sh + +dj_url: https://judge.gehack.nl +dj_url_api_suffix: api/v4 +dj_username: mart +dj_password: REDACTED + +other_url: https://judge.gehack.nl +other_url_api_suffix: api/v4 +other_username: mart +other_password: REDACTED + +cds_url: https://cds.gehack.nl +cds_url_api_suffix: api +cds_username: admin +cds_password: REDACTED +``` + +## Selectively running the playbook +The playbook has multiple tags: + - `all`: runs all tasks + - `check`: Checks the consistency of both scoreboard.json and awards.json against the CDS and other CCS when configured. + - `scoreboard`: Verifies scoreboard.json against the CDS and other CCS when configured. + - `awards`: Verifies awards.json against the CDS and other CCS when configured. + - `results.tsv`: Downloads results.tsv from DOMjudge and stores it in the repo. + - `fetch`: Retrieves all files required by the EOC including results.tsv + - `dump`: Dumps all API endpoints, specifically: + - accounts + - awards + - balloons + - clarifications + - problems + - groups + - judgements + - languages + - organizations + - runs + - scoreboard + - teams + +### Known limitations + - The checks that verify whether the awards.json are the same is *very* simple and I (Mart) have not yet found a reasonable way of actually checking them in a nice manner from ansible. Them matching would be a bigger red-flag than them having a difference. Still, the playbook fails when a difference is detected! To aid with (manual) verification the results of fetching the jsons is stored in `/tmp/awards.[sys].json` with `[sys]` either "dj", or "cds". diff --git a/provision-contest/ansible/EOC/main.yml b/provision-contest/ansible/EOC/main.yml new file mode 100644 index 00000000..047480f8 --- /dev/null +++ b/provision-contest/ansible/EOC/main.yml @@ -0,0 +1,236 @@ +--- +- hosts: localhost + connection: local + gather_facts: no + tasks: + - name: Set facts for whether enough variables are set + tags: all, compare, scoreboard, awards, fetch + ansible.builtin.set_fact: + cds_available: "{{ cds_url is defined and cds_url_api_suffix is defined and cds_username is defined and cds_password is defined }}" + other_available: "{{ other_url is defined and other_url_api_suffix is defined and other_username is defined and other_password is defined }}" + + # Do this to prevent the dreaded issues with basic auth. Tedious but it works + - name: Set DOMjudge basic auth fact + tags: all, compare, scoreboard, awards, fetch, dump + ansible.builtin.set_fact: + dj_basic_auth: "Basic {{ (dj_username+':'+dj_password) | b64encode }}" + + - name: Set CDS basic auth fact + tags: all, compare, scoreboard, awards + ansible.builtin.set_fact: + cds_basic_auth: "Basic {{ (cds_username+':'+cds_password) | b64encode }}" + when: cds_available + + - name: Set other basic auth fact + tags: all, compare, scoreboard, awards + ansible.builtin.set_fact: + other_basic_auth: "Basic {{ (other_username+':'+other_password) | b64encode }}" + when: other_available + + - name: Attempt login into DOMjudge + tags: all, fetch, results.tsv + block: + - name: Fetch CSRF from login page + ansible.builtin.uri: + method: GET + return_content: yes + url: "{{ dj_url }}/login" + register: DOMjudge_login_output + + - name: Extract CSRF input field + ansible.builtin.set_fact: + DOMjudge_csrf_field: "{{ DOMjudge_login_output.content | regex_search(']+?_csrf_token[^>]+?>') | regex_search('[^\"]{30,}') }}" + + - name: Login into DOMjudge and retrieve cookie + ansible.builtin.uri: + method: POST + body_format: form-urlencoded + url: "{{ dj_url }}/login" + status_code: 302 + headers: + cookie: "{{ DOMjudge_login_output.cookies_string }}" + body: + _username: "{{ dj_username }}" + _password: "{{ dj_password }}" + _csrf_token: "{{ DOMjudge_csrf_field }}" + register: login_response + + - name: Test succesfull login by going to a restricted page + ansible.builtin.uri: + url: "{{ dj_url }}/jury" + method: GET + headers: + cookie: "{{ login_response.set_cookie }}" + + - name: Download results.tsv + tags: all, results.tsv + ansible.builtin.uri: + url: "{{ dj_url }}/jury/import-export/export/results.tsv" + return_content: yes + method: GET + creates: "{{ repo }}/results.tsv" + dest: "{{ repo }}/results.tsv" + headers: + cookie: "{{ login_response.set_cookie }}" + + - name: Compare scoreboard.json + tags: all, compare, scoreboard + block: + - name: "Fetch scoreboard.json from Dj" + ansible.builtin.uri: + url: "{{ dj_url }}/{{ dj_url_api_suffix }}/contests/{{ contest }}/scoreboard" + dest: "/tmp/scoreboard.dj.json" + return_content: yes + force: yes + headers: + Authorization: "{{ dj_basic_auth }}" + register: dj_scoreboard + + - name: "Fetch scoreboard.json from other" + ansible.builtin.uri: + url: "{{ other_url }}/{{ other_url_api_suffix }}/contests/{{ contest }}/scoreboard" + dest: "/tmp/scoreboard.other.json" + return_content: yes + force: yes + headers: + Authorization: "{{ other_basic_auth }}" + register: other_scoreboard + when: other_available + + - name: Does Dj == other for scoreboard + block: + - name: Check for differences + shell: "{{ contestutil_loc }} ScoreboardUtil /tmp/scoreboard.dj.json /tmp/scoreboard.other.json" + when: other_available + register: cul + + - name: Print output + ansible.builtin.debug: + msg: "{{ cul.stdout | split('\n')}}" + + - name: Fail if the scoreboard does not match + ansible.builtin.fail: + msg: "Scoreboard does not match, check /tmp/scoreboard.dj.json and /tmp/scoreboard.other.json" + when: + - "'No differences found.' not in cul.stdout" + + - name: Compare awards.json + tags: all, compare, awards + block: + - name: Fetch awards.json from Dj + ansible.builtin.uri: + url: "{{ dj_url }}/{{ dj_url_api_suffix }}/contests/{{ contest }}/awards/" + dest: "/tmp/awards.dj.json" + return_content: yes + force: yes + method: GET + headers: + Authorization: "{{ dj_basic_auth }}" + register: dj_awards + + - name: Fetch awards.json from cds + ansible.builtin.uri: + url: "{{ cds_url }}/{{ cds_url_api_suffix }}/contests/{{ contest }}/awards/" + dest: "/tmp/awards.cds.json" + return_content: yes + force: yes + method: GET + headers: + Authorization: "{{ cds_basic_auth }}" + register: cds_awards + when: cds_available + + - name: Does Dj == CDS for awards + shell: "{{ jd_loc }} /tmp/awards.dj.json /tmp/awards.cds.json" + when: cds_available + + - name: "Fetch event-feed.ndjson from Dj" + tags: all, fetch + ansible.builtin.uri: + url: "{{ dj_url }}/{{ dj_url_api_suffix }}/contests/{{ contest }}/event-feed?stream=false" + return_content: yes + method: GET + creates: "{{ repo }}/event-feed.ndjson" + dest: "{{ repo }}/event-feed.ndjson" + headers: + Authorization: "{{ dj_basic_auth }}" + + - name: "Fetch {{ item }}-endpoint from Dj" + tags: all, fetch, dump + ansible.builtin.uri: + url: "{{ dj_url }}/{{ dj_url_api_suffix }}/contests/{{ contest }}/{{ item }}" + return_content: yes + method: GET + creates: "{{ repo }}/{{ item }}.json" + dest: "{{ repo }}/{{ item }}.json" + headers: + Authorization: "{{ dj_basic_auth }}" + with_items: + - accounts + - awards + - balloons + - clarifications + - problems + - groups + - judgements + - languages + - organizations + - runs + - scoreboard + - teams + + - name: "Store final_standings.html (results.html)" + tags: all, fetch + ansible.builtin.uri: + url: "{{ dj_url }}/jury/import-export/export/results.html" + creates: "{{ repo }}/final_standings.html" + dest: "{{ repo }}/final_standings.html" + headers: + cookie: "{{ login_response.set_cookie }}" + + - name: "Store (clarifications.html)" + tags: all, fetch + ansible.builtin.uri: + url: "{{ dj_url }}/jury/import-export/export/clarifications.html" + creates: "{{ repo }}/clarifications.html" + dest: "{{ repo }}/clarifications.html" + headers: + cookie: "{{ login_response.set_cookie }}" + + - name: "Store final scoreboard (contest.zip)" + tags: all, fetch + ansible.builtin.uri: + url: "{{ dj_url }}/public/scoreboard-data.zip?contest={{ contest_id }}" + creates: "{{ repo }}/final_scoreboard.zip" + dest: "{{ repo }}/final_scoreboard.zip" + headers: + cookie: "{{ login_response.set_cookie }}" + + - name: "Fetch DOMjudge submissions" + tags: all, fetch + block: + - name: Retrieve all submissions + ansible.builtin.uri: + url: "{{ dj_url }}/{{ dj_url_api_suffix }}/contests/{{ contest }}/submissions?strict=1" + return_content: yes + method: GET + headers: + Authorization: "{{ dj_basic_auth }}" + register: "submissions" + + - name: Extract submission endpoints + ansible.builtin.set_fact: + submissions: "{{ submissions.json | json_query(jmesquery) }}" + vars: + jmesquery: "[].files[?mime == 'application/zip'][]" + + - name: Download submissions + ansible.builtin.uri: + url: "{{ dj_url }}/{{ dj_url_api_suffix }}/{{ item.href }}" + return_content: yes + method: GET + headers: + Authorization: "{{ dj_basic_auth }}" + creates: "{{ repo }}/{{ item.href.split('/')[3] }}.zip" + dest: "{{ repo }}/{{ item.href.split('/')[3] }}.zip" + loop: "{{ submissions }}"