Repository: Azure/Moodle Branch: master Commit: 2afa40361348 Files: 92 Total size: 1.1 MB Directory structure: gitextract_zdgud6lr/ ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CONTRIBUTE.md ├── Gruntfile.js ├── LICENSE ├── LICENSE-DOCS ├── README.md ├── SECURITY.md ├── azuredeploy-large-ha.json ├── azuredeploy-maximal.json ├── azuredeploy-minimal.json ├── azuredeploy-small2mid-noha.json ├── azuredeploy.json ├── azuredeploy.parameters.json ├── docs/ │ ├── Cleanup.md │ ├── Deploy.md │ ├── Environment-Variables.md │ ├── Get-Install-Data.md │ ├── Manage.md │ ├── Parameters.md │ ├── Preparation.md │ ├── SslCert.md │ ├── Test.md │ └── env.json ├── env.json ├── etc/ │ ├── changeBranchInURL.sh │ ├── checkBaseUrls.sh │ ├── keyvault.sh │ ├── travis/ │ │ ├── Configuration.py │ │ ├── DeploymentTester.py │ │ └── __init__.py │ ├── travis.py │ └── updateDocsParametersMd.sh ├── images/ │ └── Moodle-Architecture-PremiumFiles.vsdx ├── loadtest/ │ ├── Azure_Login.md │ ├── Deploy_Load_Test_VM.md │ ├── README.md │ ├── azuredeploy.parameters.loadtest.defaults.json │ ├── loadtest.sh │ ├── moodle-on-azure-test-course-1.mbz │ ├── simple-test-1.jmx │ ├── simple-test-2.jmx │ ├── time-gated-exam-test-dist-slaves.jmx │ └── time-gated-exam-test.jmx ├── managedApplication/ │ ├── Cleanup.md │ ├── DeployMoodleManagedApp.md │ ├── Environment.md │ ├── PublishMoodleManagedApplication.md │ ├── README.md │ ├── createServiceCatlogUpdate.sh │ ├── createUIDefinition.json │ └── parameters-template.json ├── metadata.json ├── migration/ │ ├── azure-fileshare-sa-deploy.json │ └── azuredeploy-migration.json ├── nested/ │ ├── appgw.json │ ├── controller.json │ ├── controllersetup.json │ ├── db-mssql.json │ ├── db-mysql.json │ ├── db-mysqlflex.json │ ├── db-postgres.json │ ├── gluster.json │ ├── glustervm.json │ ├── glustervmsetup.json │ ├── network-subnets.json │ ├── network-vnet-ddos.json │ ├── network-vnet-privateDnsZone.json │ ├── network-vnet.json │ ├── network.json │ ├── nfs-ha-vm.json │ ├── nfs-ha.json │ ├── recoveryservices.json │ ├── recoveryservicesEnlist.json │ ├── redis.json │ ├── search-azure.json │ ├── search-elastic-config.json │ ├── search-elastic.json │ ├── storageAccount.json │ ├── tika.json │ ├── tikaconfig.json │ ├── vmsetupparams.json │ └── webvmss.json ├── package.json └── scripts/ ├── helper_functions.sh ├── install_elastic.sh ├── install_gluster.sh ├── install_moodle.sh ├── install_tika.sh ├── setup_nfs_ha.sh └── setup_webserver.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # test outputs loadtest/test_outputs/ # Emacs *#*# # Node Modules node_modules # Eclipse .project # Specific azuredeploy.parameters.json files for dev testing (ignored not to expose ssh pub keys) .params/ .vs/ ================================================ FILE: .jshintrc ================================================ { "esnext": true, "node": true, "browser": true, "nomen": false, "bitwise": true, "eqeqeq": true, "forin": true, "immed": true, "latedef": true, "newcap": true, "noarg": true, "noempty": true, "nonew": true, "plusplus": true, "regexp": true, "undef": true, "unused": true, "trailing": true, "indent": 4, "esnext": true, "onevar": true, "white": true, "quotmark": "double", "predef": { } } ================================================ FILE: .travis.yml ================================================ dist: trusty language: python node_js: "0.12" python: "3.5" cache: - directories: node_modules - pip env: - PYTHONUNBUFFERED=TRUE install: - npm install # Install task runners for lint checking. - pip install azure-mgmt-subscription azure-mgmt-resource keyring pycurl # Install Azure Python SDK (we only need the sub & the resource manager packages) before_script: - ssh-keygen -q -f azure_moodle_id_rsa -N "" # Generate SSH keys to send to deployment script: - npm test - ./etc/travis.py ================================================ FILE: CONTRIBUTE.md ================================================ # Contributing to Moodle on Azure The TL;DR version is: * We are a community project * We seek to make decisions through community consensus * We prefer debate through gradual improvement through pull requests to endless discussion about the "perfect" solution * We are a meritocracy, not a democracy * We welcome all your contributions including but not limited to feature requests, bug-reports, documentation and code ## How the project is managed This project welcomes contributions and suggestions. Our goal is to work on Azure specific tooling for deploying and managing the open source [Moodle](http://moodle.org) learning management system on Azure. We do not work on Moodle itself here, instead we work upstream as appropriate. The short version of how to contribute to this project is "just do it". Where "it" can be defined as any valuable contribution (and to be clear, asking questions is a valuable contribution): * ask questions * provide feedback * write or update documentation * help new users * recommend the project to others * test the code and report bugs * fix bugs and issue pull requests * give us feedback on required features * write and update the software * create artwork * translate to different languages * anything you can see that needs doing Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. ## Decision Making This is a community project. Decisions are made through consensus building. All voices are equal and we welcome input from everyone. That said, this is not a democracy. Consensus does not mean everyone has to agree. It merely means that nobody is objecting *and* offering an alternative. What this means, in practive, is that she who does the work makes the decisions. We'd rather discuss how to improve imperfect code than argue over what would make perfect code. So if you have an objection to the way we are doing things issue a pull request. In the unlikely event that we cannot reach consensus through consensus then the project maintainers (as identified by their having the admin bit on GitHub) will make a judgetment call. But normally their role is to guide the community to consensus action, not to make decisions on bhalf of the community. ## Minimum Bar for Contributions As the project matures we will add more thorough testing. It is expected that all contributions pass the currently available suite of tests. If they do not then they will be rejected. It is also required that contributions which add features also bring at least basic testing of that feature. ## Planning This is an open source project. We have a few mantras to ensure efficient collaboration, these mostly boil down to ensuring good visibility into the communities goals. These include: * If it didn't happen in public, it didn't happen * Scratch your own itch ### If it didn't happen in public, it didn't happen (aka full transparency) The goal of this mantra is to ensure maximum visibility into our communities work in order to: 1. Provide an opportunity for community feedback in order to ensure our plans are good 2. Provide a clear indication of what will be done, what may be done and what won't be done Both of these goals lead to the second mantra "Scratch your own itch". ### Scratch your own itch (aka getting what you want) This is an open source project. We welcome feature requests and, as a community, we will provide feedback on whether we intend to work on it or not. To this end we categories feature requests in one of 4 ways: * Priority 0 (will address) * Priority 1 (may address) * Priority 2 (maybe one day) * wontfix (out of scope) Using these priorities it is easy for community members to decide where to spend their time. For example: * Priority 0 items are actively being worked on by at least one community member. Others are welcome to contribute as appropriate (reviews are particularly important) * Priority 1 items are seen as important and are likely to be worked on in the short to medium term, but there is no community member active on the project at this time. Community members are welcome to take ownership of these issues and propose a solution that they intend to implement. If the community accepts the proposal then it will become a Priority 0 issue. * Priority 2 items are seen as interesting proposals that are not in conflict with the projects goals but are unlikely to be worked on by any existing communty members. Community members who have a need for these items are strongly encouraged to identify themselves and offer a proposal for a solution. If there is enough support within the existing community this item can become a Priority 0 under your leadership. * Wontfix items are considered out of scope for this project. Community members should seek to solve the problem in different ways. Often this will mean contribution to Moodle itself or a plugin that is external to this community. ## Community roles This section outlines roles and responsibilities within the community. ### Users Users self-identify by using our software and documentation. Their responsibilities are to benefit from our work, but we welcome contributions from users, such as: * Ask questions * Answer questions * Feature requests * Bug reports * Design reviews * Planning reviews * Evangelize the project * and more... Some users will become more involved with the project, those users become Contributors. ### Contributors Contributes self-identify by making longer term commitments to our project. Their responsibilities are to help the project be succesful by ensuring that our work matches the needs of our users. Possible contributions can include: * Everything a User might contribute * Remove blocks for users * Provide design input * Review pull requests * Implement features * Triage questions, feature requests and bug reports * and more... Some contributors will become very engaged and therefore become an essential part of the community, these contributors will become Maintainers. ### Maintainers We are fans of efficient processes. Maintainers are people who insert themselves into our process to ensure they run well. The goal is to empower our contributors who in turn focus on delighting our users. Maintainers contributions may include: * Everyting Users and Contributors do * Merge pull requests where appropriate * Seek community consensus where conflict occurs * Remove blocks for contributors * and more... ## Pull requests, Review and Merges We like efficient processes. Anyone is welcome to issue pull requests. Everyone is encouraged to review pull requests. Maintainers are responsible for merging pull requests but they are not responsible for reviews, that is a community wide responsibility. We operate under two models of review process as appropriate to each circumstance: * Merge then Review (our preferred model) * Review then Merge ### Merge Then Review In the "merge then review" model a maintainer will merge the pull request into with minimal review. Community members are still expected to review the code, but it is done after the fact. The goal is to get the code into a shared repository as early as possible. This allows people, including advanced users, to start testing it. This ensures we have the maximum possible exposure to testing in real scenarios early in the process. Encouragin bug reports from the whole community ensures we have visibility into breaks as early as possible. This model has its risks, however. If a PR is on the critical path or it is controversial in some way it is expected that maintainers will ensure it recieves a thorough review before merging (see next section on "Review then Merge". This decision is at the discretion of the maintainer who first triages the pull request. Should a mistake be made and a bad merge be performed then it can often be easier and faster to fix it under the "Merge then Review" model than it is to provide feedback to the original author and await a fix from them. Should the mistake have a high impact and/or no easy fix is available we simply roll back the merge and provide feedback via the review process. It should be noted that this model means that maintainers have the right to simply merge their own code and expect others to review it *after*. Maintainers are expected to use their best judgement when excercising this priviledge. ### Review Then Merge Where a change is on the critical path or it is potentiall contriversial maintainers should request reviews using the GitHub tooling. The last reviewer to sign-off on the pull request will merge the pull request. ================================================ FILE: Gruntfile.js ================================================ var grunt = require('grunt'); require('load-grunt-tasks')(grunt); var templates = ['nested/*.json', 'managedApplication/*.json', 'loadtest/*.json', '*.json']; grunt.initConfig({ jshint: { files: templates, options: { jshintrc: '.jshintrc' } } }); grunt.registerTask('test', ['jshint']); ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) Microsoft Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LICENSE-DOCS ================================================ Attribution 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More_considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. ================================================ FILE: README.md ================================================ # Deploy and Manage a Scalable Moodle Cluster on Azure This repository contains guides and [Azure Resource Manager](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-overview) templates designed to help you deploy and manage a highly available and scalable [Moodle](https://moodle.com) cluster on Azure. In addition, the repository contains other useful information relevant to running Moodle on Azure such as a listing of Azure-relevant Moodle plugins and information on how to offer Moodle as a Managed Application on the Azure Marketplace or on an IT Service Catalog. If you have an Azure account you can deploy Moodle via the [Azure portal](https://portal.azure.com) using the button below, or you can [deploy Moodle via the CLI](docs/Deploy.md). Please note that while you can use an [Azure free account](https://azure.microsoft.com/en-us/free/) to get started depending on which template configuration you choose you will likely be required to upgrade to a paid account. ## Deployment Introduction In the table below, we provide a number of default configurations at different scales of operation. These options minimize the configuration you would otherwise need to do manually; these options are essentially "good practice" recommendations. Once deployed, you will have full access to the Azure resources and can adjust the deployment to suit your needs. If you would prefer to have full control over all the configuration options at deployment, please refer to [the fully configurable section](#Fully Configurable) right after the Predefined deployment option section. ## SSH Key Requirement All of the deployment options require you to provide a valid SSH protocol 2 (SSH-2) RSA public-private key pairs with a minimum length of 2048 bits. Other key formats such as ED25519 and ECDSA are not supported. If you are unfamiliar with SSH and SSH keys, read this [article](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/mac-create-ssh-keys) which will explain how to generate a key pair. You will create a ssh key pair. The public key is copied to the instances via the template. The private key is your identity that you will use to connect to different parts of the service. ## Predefined deployment options Below are a list of pre-defined/restricted deployment options based on typical deployment scenarios (i.e. dev/test, production etc.) All configurations are fixed and you just need to pass your ssh public key to the template so that you can log in to the deployed VMs. | Deployment Type | Description | Launch | | --- | --- | ---| | Minimal | This deployment will use NFS, Azure Database for MySQL Flexible Server (Burstable SKU 2 vCores), and smaller autoscale web frontend VM sku (1 core) that'll give faster deployment time (less than 30 minutes) and requires only 2 VM cores currently that'll fit even in a free trial Azure subscription.|[![Deploy to Azure Minimally](http://azuredeploy.net/deploybutton.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FMoodle%2Fmaster%2Fazuredeploy-minimal.json)| | Small to Mid-Size | Supporting up to 1000 concurrent users. This deployment will use NFS (no high availability) and Azure Database for MySQL Flexible Server(General Purpose SKU 8 vCores), without other options like elastic search or redis cache.|[![Deploy to Azure Minimally](http://azuredeploy.net/deploybutton.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FMoodle%2Fmaster%2Fazuredeploy-small2mid-noha.json)| |Large size deployment (with high availability)| Supporting more than 2000 concurrent users. This deployment will use Azure Premium Files, Azure Database for MySQL Flexible Server (General Purpose SKU 16 vCores) and redis cache, without other options like elastic search. |[![Deploy to Azure Minimally](http://azuredeploy.net/deploybutton.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FMoodle%2Fmaster%2Fazuredeploy-large-ha.json)| | Maximum |This maximal deployment will use Azure Premium Files, Azure Database for MySQL (Business Critical SKU 64 vCores), redis cache, elastic search (3 VMs), and pretty large storage sizes (both data disks and DB).|[![Deploy to Azure Maximally](http://azuredeploy.net/deploybutton.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FMoodle%2Fmaster%2Fazuredeploy-maximal.json)| **NOTE**: The above deployment templates use hard coded Azure Database for MySQL Flexible Server SKUs for easier configuration and quicker deployment of Moodle workloads. If your deployment fails for any reason, please revert to the fully configurable template where possible and change the Azure Database for MySQL Flexible Server parameters accordingly. ## Fully Configurable If you would prefer to configure the deployment right at the start of the process, you use the button below. Please note that this method opens up a large number of parameters to configure and users new to this deployment process may find it overwhelming. It is also very likely you may end up with a deployment configuration that is not optimal to your needs. This method is recommended for power users. [![Deploy to Azure Fully Configurable](http://azuredeploy.net/deploybutton.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FMoodle%2Fmaster%2Fazuredeploy.json) [![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.png)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FMoodle%2Fmaster%2Fazuredeploy.json) **NOTE**: Before you deploy your Moodle workloads using a fully configurable template, we suggest reviewing [Azure Database for MySQL Flexible Server](https://learn.microsoft.com/en-us/azure/mysql/flexible-server/) documentation to fully understand the parameters and the options suggested in the parameters to pick the right values for your workload needs. ## Stack Architecture This template set deploys the following infrastructure core to your Moodle instance: - Autoscaling web frontend layer (Nginx for https termination, Varnish for caching, Apache/php or nginx/php-fpm) - Private virtual network for frontend instances - Controller instance running cron and handling syslog for the autoscaled site - [Azure Load balancer](https://azure.microsoft.com/en-us/services/load-balancer/) to balance across the autoscaled instances - [Azure Database for MySQL](https://azure.microsoft.com/en-us/services/mysql/) or [Azure Database for PostgreSQL](https://azure.microsoft.com/en-us/services/postgresql/) or [Azure SQL Database](https://azure.microsoft.com/en-us/services/sql-database/) - For large production deployments Azure Premium Files is recommended for file storage as it provides higher performance and availability with the addition of metadata-caching [https://learn.microsoft.com/en-us/azure/storage/files/smb-performance#metadata-caching-for-premium-smb-file-shares](https://learn.microsoft.com/en-us/azure/storage/files/smb-performance#metadata-caching-for-premium-smb-file-shares) This template set *optionally* configures the following additional infrastructure: - [Azure Backup](https://azure.microsoft.com/en-us/services/backup/) for Moodle site backups - [Azure Blob Storage](https://azure.microsoft.com/en-us/services/storage/blobs/) for ObjectFS (Moodle sitedata) - [Azure Application Gateway](https://azure.microsoft.com/en-us/services/application-gateway/) for SSL offloading and WAF - [Azure Redis Cache](https://azure.microsoft.com/en-us/services/cache/) instance for Moodle caching - [Azure DDoS Protection](https://azure.microsoft.com/en-us/services/ddos-protection/) plan to secure your Moodle site from DDoS attacks - [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) for storing your CA Cert for your Moodle site - [Azure Search](https://azure.microsoft.com/en-us/services/search/) instance or three Elasticsearch VMs for HA Global Search in Moodle - [Apache Tika](http://tika.apache.org/) VMs for search indexing in Moodle ![network_diagram](images/Moodle-Architecture-PremiumFiles.png "Diagram of deployed stack") The template also optionally installs plugins that allow Moodle to be integrated with select Azure services (see below for details). ## Useful Moodle plugins for integrating Moodle with Azure Services There below is a listing of useful plugins allow Moodle to be integrated with select Azure services: - [Object File System Plugin*](https://github.com/catalyst/moodle-tool_objectfs) for [Azure Blob Storage](https://azure.microsoft.com/en-us/services/storage/blobs/) - [Azure Search Plugin*](https://github.com/catalyst/moodle-search_azure) for [Azure Search](https://azure.microsoft.com/en-us/services/logic-apps/) - [Trigger Plugin](https://github.com/catalyst/moodle-tool_trigger) and [Restful Webservice Plugin](https://github.com/catalyst/moodle-webservice_restful) for [Azure Logic Apps](https://azure.microsoft.com/en-us/services/logic-apps/) (requires use of [Moodle Connector](https://github.com/catalyst/azure-connector_moodle) now in development) - [Office 365 and Microsoft Entra ID (formerly Azure Active Directory) Plugins for Moodle*](https://github.com/Microsoft/o365-moodle) for [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id/) - [Elasticsearch Plugin*](https://github.com/catalyst/moodle-search_elastic) At the current time this template allows the optional installation of the plugins above with a * next to them. Please note these plugins can be installed at any time post deployment via Moodle's own [plugin directory](https://moodle.org/plugins/). You can find a list of all Azure relevant plugins in the Moodle plugin directory [here](https://moodle.org/plugins/browse.php?list=set&id=91). You might also choose to follow this list via RSS. ## Moodle as a Managed Application You can learn more about how you can offer Moodle as a Managed Application on the Azure Marketplace or on an IT Service Catalog [here](https://github.com/Azure/Moodle/tree/master/managedApplication). This is a great read if you are offering Moodle hosting services today for your customers. ## Observations about the current template The template is highly configurable. Full details of the configuration options can be found in our [documentation](https://github.com/Azure/Moodle/tree/master/docs) (more specifically in our [parameters documentation](https://github.com/Azure/Moodle/blob/master/docs/Parameters.md)). The following sections describe observations about the template that you will likely want to review before deploying: **Scalability** Our system is designed to be highly scalable. To achieve this we provide a Virtual Machine Scaleset for the web tier. This is already configured to scale on high load. However, scaling the VMs is not instantaneous. If you know you will have a high-load situation(e.g. exam, you should manually scale the VMs prior to the event. This can be done through the Azure portal or the CLI. The database is less easily scaled at this point, but it is possible and documented in our [management documentation](https://github.com/Azure/Moodle/blob/master/docs/Manage.md#resizing-your-database). **SSL** The template fully supports SSL but it is not possible for the template to manage this for you. More information in our [managing certs documentation](https://github.com/Azure/Moodle/blob/master/docs/SslCert.md). **Moodle PHP Code** The Moodle PHP code is stored on the Controller VM and copied to each front end VM upon deployment and upon request (should you update the Moodle code with your own code). For more information see our [management documentation](https://github.com/Azure/Moodle/blob/master/docs/Manage.md#updating-moodle-codesettings). **Database** Currently the best performance is achieved with [Azure Database for MySQL](https://azure.microsoft.com/en-us/services/mysql/) and [Azure SQL Database](https://azure.microsoft.com/en-us/services/sql-database/). With [Azure Database for PostgreSQL](https://azure.microsoft.com/en-us/services/postgresql/) we have hit database constraints which caused processes to load up on the frontends until they ran out of memory. It is possible some PostgreSQL tuning might help here. Above pre-configured deployment templates deploy Azure Database for MySQL Flexible Server in a VNet. For configuring Azure Database for MySQL Flexible Server outside a VNet to use firewall-based IP restriction, please use the fully configurable template. **File Storage** There are two options for file storage (moodledata) - Azure Premium Files or NFS. Azure Premium Files is recommended for production deployments as it provides higher performance and availability with the addition of metadata-caching [https://learn.microsoft.com/en-us/azure/storage/files/smb-performance#metadata-caching-for-premium-smb-file-shares](https://learn.microsoft.com/en-us/azure/storage/files/smb-performance#metadata-caching-for-premium-smb-file-shares). Non-HA NFS is recommended for dev/test deployments. **Search.** Azure supports running an Elasticsearch cluster, however it does not offer a fully-managed Elasticsearch service, so for those looking for a fully-managed Search service [Azure Search](https://azure.microsoft.com/en-us/services/logic-apps/) is recommended. **Caching.** While enabling Redis cache can improve performance for a large Moodle site we have not seen it be very effective for small-to-medium size sites. We can likely improve upon this, patches welcome ;-) **Regions.** Note that not all resources types (such as databases) may be available in your region. You should check the list of [Azure Products by Region](https://azure.microsoft.com/en-us/global-infrastructure/services/) to for local availability. ## Common questions about this Template 1. **Is this template Moodle as IaaS or PaaS?** While the current template leverages PaaS services such as Redis, Azure Database for MySQL Flexible Server, Azure Database for Postgres, MS SQL etc. the current template offers Moodle as IaaS. Given limitations to Moodle our focus is IaaS for the time being however we would love to be informed of your experience running Moodle as PaaS on Azure (i.e. using [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/) or [Azure App Service](https://azure.microsoft.com/en-us/services/container-service/)). 2. **The current template uses Ubuntu. Will other Operating Systems such as CentOS or Windows Server be supported in the future?** Unfortunately we only have plans to support Ubuntu at this time. It is highly unlikely that this will change. 3. **What configuration do you recommend for my Moodle site?** The answer is it depends. At this stage we provide some rudimentary t-shirt sized deployment recommendations and we are still building out our load testing tools and methodologies to provide more granularity. With that being said this is an area we are investing heavily in this area and we would love your contributions (i.e. load testing scripts, tools, methodologies etc.). If you have an immediate need for guidance for a larger sized deployment, you might want to share some details around your deployment on our [issues page](https://github.com/Azure/Moodle/issues) and we will do our best to respond. Please share as much information about your deployment as possible such as: - average number of concurrent users your site will see - maximum level of concurrent/simultaneous users your site needs to support - whether or not HA is needed - any other attributes specific to your deployment (i.e. load balancing across regions etc.) 4. **Did Microsoft build this template alone or with the help of the Moodle community?** We did not build this template alone. We relied on the expertise and guidance of many capable Moodle partners around the world. The initial implementation of the template was done by [Catalyst IT](https://github.com/catalyst). 5. **How does this template relate to other Moodle offerings available on the Azure Marketplace?** It is generally not a good idea to run Moodle as a single VM in a production setting. This template is highly configurable and allows for high availability and redundancy. 6. **How does this template relate to this [Azure Quickstart Template for Moodle](https://github.com/Azure/azure-quickstart-templates/tree/master/application-workloads/moodle/moodle-scalable-cluster-ubuntu)?** This repo is the working repo for the quickstart template. We will be pushing changes from this template to the quickstart template on a regular cadence. 7. **I am already running Moodle on Azure. How does this work benefit me?** We are looking for painpoints from you and the broader Moodle on Azure community that we can help solve. We are also looking to understand where our implementation of Moodle on Azure outperforms or underperforms other implementations such as yours that are out in the wild. If you have observations, performance benchmarks or just general feedback about your experience running Moodle on Azure that you'd like to share we're extremely interested! Load testing is a very big area of focus, so if you have scripts you wouldn't mind contributing please let us know. 8. **Has anyone run this template sucessfully in production?** Yes they have. With that being said, we do not make any performance guarantees about this architecture. 9. **What type of improvements have you succeeded in making** Since we first began this effort we have managed to make great gains, achieving a >2x performance boost from our original configuration by making tweaks to things like where PHP files were stored. Our work is nowhere near over. 10. **What other Azure services (i.e. [Azure CDN](https://azure.microsoft.com/en-us/services/cdn/), [Azure Media Services](https://azure.microsoft.com/en-us/services/media-services/), [Azure Bot Service](https://azure.microsoft.com/en-us/services/bot-service/) etc.) will you be integrating with when this effort is complete?** It's not clear yet. We'll need your [feedback](https://github.com/Azure/Moodle/issues) to decide. 11. **Why is the database on a public subnet?** At this stage only Azure Database for PostgreSQL do not support being moved to a vnet. As a workaround, we use a firewall-based IP restriction allow access only to the controller VM and VMSS load-balancer IPs. 12. **Is Azure Database for MySQL Flexible Server deployed in a VNet?** When you leverage one of the pre-defined template options, we automatically deploy your Azure Database for MySQL Flexible Server in VNet for better isolation and greater security, optionally you can choose the fully configurable template to deploy Azure Database for MySQL Flexible Server outside VNet depending on your needs. 13. **How can I help with this effort?** Please see below. ## Automated Testing (Travis CI) This repository uses [Travis CI](https://travis-ci.org/) to deliver automated testing. The following tests are carried out for every Pull Request and will also run in a Travis CI enabled forked repository: - **JSON Linting** - All JSON files are linted to ensure they do not contain any syntax errors. - **JSON Code Style** - All JSON files are tested to ensure they comply with project code style rules. The following tests are carried out as part of the Pull Request merging prior to a contribution being accepted into the release branch: - **Template Validation** - The template is submitted to Azure to ensure it is correctly formatted and contains valid logic. - **Template Build** - The template is submitted to Azure and the stack described in the template is built to ensure a stack is correctly deployed. ### Setting Up Travis CI for Template Build The following describes the process required if you want to run the template validation and build steps using your own Travis and Azure accounts. To set up the build process, you will need: - An Azure account or active subscription - A fork of this repository linked to Travis CI - Access to an installed instance of the Azure CLI - A SSH key-pair The Travis CI process uses the *Azure CLI Service Principal* login method to authenticate against Azure. The documentation for logging in via a Service Principal can be found here: [https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli?view=azure-cli-latest#logging-in-with-a-service-principal](https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli?view=azure-cli-latest#logging-in-with-a-service-principal) Before you can log in using the Service Principal process you need to create a *Service Principal*. The documentation to create a Service Principal login can be found here: [https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli) When a Service Principal is created using the Azure CLI a JSON response is returned containing: - **name** - This is the Service Principal username. - **password** - This is the Service Principal password. - **tenantId** - This is the Service Principal tenant unique ID. You will need these three above values to have Travis and Azure deploy and test your template. The next step is to take the above values returned by the Service Principal creation and use them to define *environment variables* in Travis CI. The following link shows how to set up per repository environment variables in Travis CI: [https://docs.travis-ci.com/user/environment-variables/#Defining-Variables-in-Repository-Settings](https://docs.travis-ci.com/user/environment-variables/#Defining-Variables-in-Repository-Settings) Using this documentation set up the following three *hidden* environment variables in Travis CI for your fork of this repository. - **SPNAME** - The value of the *name* parameter returned by the Service Principal create process. - **SPPASSWORD** - The value of the *password* parameter returned by the Service Principal create process. - **SPTENANT** - The value of the *tenant* parameter returned by the Service Principal create process. - **SPSSHKEY** *(default: generate new)*- A public SSH key that you have the corresponding private key for. This is currently not used but is required for the build to be successful. - **LOCATION** *(default: southcentralus)*- Location for the test resource group. - **RESOURCEGROUP** *(default: azmdl-travis-XXX)*- Name to use for the resource group. - **FULLCI_BRANCHES** *(default: master)*- Name of branches (separated by ':') to always run FULL CI (if credentials are provided). Full CI will run a deployment test which will create and use resources from your Azure account. **NOTE:** You can trigger a full CI test by adding *[full ci]* or *[fullci]* anywhere in the commit message. **NOTE:** Make sure you set the environment variables to hidden otherwise they will be exposed publicly at run time. **NOTE:** As per the Travis CI documentation make sure you have correctly escaped the environment variable values when they are defined. Once the environment variables are defined, Travis CI will run the template validate and build steps as part of the test process. ## Contributing This project welcomes contributions and suggestions. Our goal is to work on Azure specific tooling for deploying and managing the open source [Moodle](http://moodle.org) learning management system on Azure. We do not work on Moodle itself here, instead we work upstream as appropriate. The short version of how to contribute to this project is "just do it". Where "it" can be defined as any valuable contribution (and to be clear, asking questions is a valuable contribution): - ask questions - provide feedback - write or update documentation - help new users - recommend the project to others - test the code and report bugs - fix bugs and issue pull requests - give us feedback on required features - write and update the software - create artwork - translate to different languages - anything you can see that needs doing For a more detailed discussion of how to contribute see our [Contribution Guide](CONTRIBUTE.md). ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Legal Notices Microsoft and any contributors grant you a license to the Microsoft documentation and other content in this repository under the [Creative Commons Attribution 4.0 International Public License](https://creativecommons.org/licenses/by/4.0/legalcode), see the [LICENSE](LICENSE) file, and grant you a license to any code in the repository under the [MIT License](https://opensource.org/licenses/MIT), see the [LICENSE-CODE](LICENSE-CODE) file. Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at [http://go.microsoft.com/fwlink/?LinkID=254653](http://go.microsoft.com/fwlink/?LinkID=254653). Privacy information can be found at [https://privacy.microsoft.com/en-us/](https://privacy.microsoft.com/en-us/) Microsoft and any contributors reserve all others rights, whether under their respective copyrights, patents, or trademarks, whether by implication, estoppel or otherwise. ## Next Steps 1. [Deploy a Moodle Cluster](docs/Deploy.md) 1. [Obtain Deployment Details about a Moodle Cluster](docs/Get-Install-Data.md) 1. [Delete a Moodle Cluster](docs/Delete.md) ================================================ FILE: SECURITY.md ================================================ ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). ================================================ FILE: azuredeploy-large-ha.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "_artifactsLocation": { "type": "string", "metadata": { "description": "The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated." }, "defaultValue": "https://raw.githubusercontent.com/Azure/Moodle/master/" }, "_artifactsLocationSasToken": { "type": "securestring", "metadata": { "description": "The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated." }, "defaultValue": "" }, "sshPublicKey": { "metadata": { "description": "ssh public key" }, "type": "string" }, "mysqlflexHaEnabled": { "allowedValues": [ "Disabled", "SameZone", "ZoneRedundant" ], "defaultValue": "Disabled", "metadata": { "description": "High availability mode for azure database for mysql flexible server." }, "type": "string" } }, "resources": [ { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "mainTemplate", "properties": { "mode": "Incremental", "parameters": { "_artifactsLocation": { "value": "[parameters('_artifactsLocation')]" }, "_artifactsLocationSasToken": { "value": "[parameters('_artifactsLocationSasToken')]" }, "redisDeploySwitch": { "value": true }, "sshPublicKey": { "value": "[parameters('sshPublicKey')]" }, "autoscaleVmCountMax": { "value": 20 }, "autoscaleVmSku": { "value": "Standard_DS3_v2" }, "searchType": { "value": "elastic" }, "dbServerType": { "value": "mysqlflex" }, "vnetDbDeploySwitch": { "value": true }, "mysqlflexSkuName": { "value": "Standard_D16ds_v4" }, "mysqlflexStgIops": { "value": 5000 }, "mysqlflexStgSizeGiB": { "value": 512 }, "mysqlflexHaEnabled": { "value": "[parameters('mysqlflexHaEnabled')]" }, "fileServerType": { "value": "azurefiles" }, "fileServerDiskSize": { "value": 1024 }, "storageAccountType": { "value": "Premium_LRS" }, "loadBalancerSku": { "value": "Standard" }, "OSDiskSizeInGB": { "value": 1024 } }, "templateLink": { "uri": "[concat(parameters('_artifactsLocation'), 'azuredeploy.json', parameters('_artifactsLocationSasToken'))]" } } } ], "outputs": { "mainTemplateOutputs": { "type": "object", "value": "[reference('mainTemplate').outputs]" } }, "variables": { "documentation01": "This wrapper template calls the main-template with pre-defined configs for large size workloads (with high availability) and the only required parameter (sshPublicKey).", "documentation02": "For the expected small-to-mid size workloads, other parameters are fixed in this template and overridden as above." } } ================================================ FILE: azuredeploy-maximal.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "_artifactsLocation": { "type": "string", "metadata": { "description": "The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated." }, "defaultValue": "https://raw.githubusercontent.com/Azure/Moodle/master/" }, "_artifactsLocationSasToken": { "type": "securestring", "metadata": { "description": "The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated." }, "defaultValue": "" }, "sshPublicKey": { "metadata": { "description": "ssh public key" }, "type": "string" }, "mysqlflexHaEnabled": { "allowedValues": [ "Disabled", "SameZone", "ZoneRedundant" ], "defaultValue": "Disabled", "metadata": { "description": "High availability mode for azure database for mysql flexible server." }, "type": "string" } }, "resources": [ { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "mainTemplate", "properties": { "mode": "Incremental", "parameters": { "_artifactsLocation": { "value": "[parameters('_artifactsLocation')]" }, "_artifactsLocationSasToken": { "value": "[parameters('_artifactsLocationSasToken')]" }, "redisDeploySwitch": { "value": true }, "azureBackupSwitch": { "value": true }, "searchType": { "value": "elastic" }, "sshPublicKey": { "value": "[parameters('sshPublicKey')]" }, "dbServerType": { "value": "mysqlflex" }, "vnetDbDeploySwitch": { "value": true }, "mysqlflexSkuName": { "value": "Standard_E64ds_v4" }, "mysqlflexStgIops": { "value": 10000 }, "mysqlflexStgSizeGiB": { "value": 1024 }, "mysqlflexHaEnabled": { "value": "[parameters('mysqlflexHaEnabled')]" }, "fileServerType": { "value": "azurefiles" }, "fileServerDiskSize": { "value": 1024 }, "storageAccountType": { "value": "Premium_LRS" }, "autoscaleVmSku": { "value": "Standard_DS3_v2" } }, "templateLink": { "uri": "[concat(parameters('_artifactsLocation'), 'azuredeploy.json', parameters('_artifactsLocationSasToken'))]" } } } ], "outputs": { "mainTemplateOutputs": { "type": "object", "value": "[reference('mainTemplate').outputs]" } }, "variables": { "documentation01": "This wrapper template calls the main-template with maximum configs and the only required parameter (sshPublicKey).", "documentation02": "For the best-possible performance, highly available, and most Moodle features, other parameters are fixed in this template and overridden as above." } } ================================================ FILE: azuredeploy-minimal.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "_artifactsLocation": { "type": "string", "metadata": { "description": "The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated." }, "defaultValue": "https://raw.githubusercontent.com/Azure/Moodle/master/" }, "_artifactsLocationSasToken": { "type": "securestring", "metadata": { "description": "The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated." }, "defaultValue": "" }, "sshPublicKey": { "metadata": { "description": "ssh public key" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Resources/deployments", "apiVersion": "2021-01-01", "name": "mainTemplate", "properties": { "mode": "Incremental", "parameters": { "_artifactsLocation": { "value": "[parameters('_artifactsLocation')]" }, "_artifactsLocationSasToken": { "value": "[parameters('_artifactsLocationSasToken')]" }, "redisDeploySwitch": { "value": false }, "sshPublicKey": { "value": "[parameters('sshPublicKey')]" }, "autoscaleVmSku": { "value": "Standard_DS1_v2" }, "enableAccelNwForOtherVmsSwitch": { "value": false }, "dbServerType": { "value": "mysqlflex" }, "vnetDbDeploySwitch": {"value": true }, "fileServerDiskCount": { "value": 2 }, "fileServerDiskSize": { "value": 32 } }, "templateLink": { "uri": "[concat(parameters('_artifactsLocation'), 'azuredeploy.json', parameters('_artifactsLocationSasToken'))]" } } } ], "outputs": { "mainTemplateOutputs": { "type": "object", "value": "[reference('mainTemplate').outputs]" } }, "variables": { "documentation01": "This wrapper template calls the main-template with bare minimum configs and the only required parameter (sshPublicKey).", "documentation02": "To speed up deployment and consume least resources, other parameters are fixed in this tempalte and overriden as follows:", "documentation03": " - fileServerType: nfs", "documentation04": " - autoscaleVmSku: Standard_DS1_vs", "documentation05": " - fileServerDiskCount: 2", "documentation06": " - dbServerType: mysqlflex", "documentation07": " - redisDeploySwitch: false" } } ================================================ FILE: azuredeploy-small2mid-noha.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "_artifactsLocation": { "type": "string", "metadata": { "description": "The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated." }, "defaultValue": "https://raw.githubusercontent.com/Azure/Moodle/master/" }, "_artifactsLocationSasToken": { "type": "securestring", "metadata": { "description": "The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated." }, "defaultValue": "" }, "sshPublicKey": { "metadata": { "description": "ssh public key" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "mainTemplate", "properties": { "mode": "Incremental", "parameters": { "_artifactsLocation": { "value": "[parameters('_artifactsLocation')]" }, "_artifactsLocationSasToken": { "value": "[parameters('_artifactsLocationSasToken')]" }, "redisDeploySwitch": { "value": false }, "sshPublicKey": { "value": "[parameters('sshPublicKey')]" }, "dbServerType": { "value": "mysqlflex" }, "vnetDbDeploySwitch": {"value": true }, "mysqlflexSkuName": { "value": "Standard_D8ds_v4" }, "mysqlflexStgIops": { "value": 1000 }, "mysqlflexStgSizeGiB": { "value": 128 }, "fileServerDiskSize": { "value": 128 } }, "templateLink": { "uri": "[concat(parameters('_artifactsLocation'), 'azuredeploy.json', parameters('_artifactsLocationSasToken'))]" } } } ], "outputs": { "mainTemplateOutputs": { "type": "object", "value": "[reference('mainTemplate').outputs]" } }, "variables": { "documentation01": "This wrapper template calls the main-template with pre-defined configs for small-to-mid size workloads (without high availability) and the only required parameter (sshPublicKey).", "documentation02": "For the expected small-to-mid size workloads, other parameters are fixed in this template and overridden as above." } } ================================================ FILE: azuredeploy.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "_artifactsLocation": { "type": "string", "metadata": { "description": "The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated." }, "defaultValue": "https://raw.githubusercontent.com/Azure/Moodle/master/" }, "_artifactsLocationSasToken": { "type": "securestring", "metadata": { "description": "The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated." }, "defaultValue": "" }, "applyScriptsSwitch": { "defaultValue": true, "metadata": { "description": "Switch to process or bypass all scripts/extensions" }, "type": "bool" }, "azureBackupSwitch": { "defaultValue": false, "metadata": { "description": "Switch to configure AzureBackup and enlist VM's" }, "type": "bool" }, "redisDeploySwitch": { "defaultValue": false, "metadata": { "description": "Switch to deploy a redis cache or not. Note that certain versions of Moodle (e.g., 3.1) don't work well with Redis, so use this only for known well-working Moodle versions (e.g., 3.4)." }, "type": "bool" }, "vnetGwDeploySwitch": { "defaultValue": false, "metadata": { "description": "Switch to deploy a virtual network gateway or not" }, "type": "bool" }, "installObjectFsSwitch": { "defaultValue": false, "metadata": { "description": "Switch to install Moodle Object FS plugins (with Azure Blob storage)" }, "type": "bool" }, "installO365pluginsSwitch": { "defaultValue": false, "metadata": { "description": "Switch to install Moodle Office 365 plugins. As of May 22, 2018, O365 plugins for Moodle 3.5 haven't been released, so to set this true, you must set the moodleVersion to 3.4 or below." }, "type": "bool" }, "installGdprPluginsSwitch": { "defaultValue": false, "metadata": { "description": "(Should be used only for Moodle 3.4 & 3.3) Switch to install Moodle GDPR plugins. Note these require Moodle versions 3.4.2+ or 3.3.5+ and these are included by default in Moodle 3.5. So if you choose MOODLE_35_STABLE as your moodleVersion, do not set this to true." }, "type": "bool" }, "htmlLocalCopySwitch": { "defaultValue": true, "metadata": { "description": "Switch to create a local copy of /moodle/html or not" }, "type": "bool" }, "ddosSwitch": { "defaultValue": false, "metadata": { "description": "Switch to create a DDoS protection plan" }, "type": "bool" }, "enableAccelNwForCtlrVmSwitch": { "defaultValue": false, "metadata": { "description": "Switch to enable Azure Accelerated Networking on the controller VM. Default to false because currently the default controller VM SKU (D1) doesn't support AN. Change this to true if you set the controller VM SKU to eligibible ones (e.g., D2) for better performance." }, "type": "bool" }, "enableAccelNwForOtherVmsSwitch": { "defaultValue": true, "metadata": { "description": "Switch to enable Azure Accelerated Networking on all other VMs. Default to true because currently the default controller VM SKU for all other VMS (D2) does support AN. Change this to false if you set the SKU of any other VMs to an ineligibible one (e.g., D1) to avoid deployment failure." }, "type": "bool" }, "vnetDbDeploySwitch": { "defaultValue": false, "metadata": { "description": "Switch to deploying the db in VNet. Note: currently the Vnet DB Deployment is available for mysqlflex only. For other db server types, the database will not be deployed in VNet." }, "type": "bool" }, "httpsTermination": { "allowedValues": [ "VMSS", "AppGw", "None" ], "defaultValue": "VMSS", "metadata": { "description": "Indicates where https termination occurs. 'VMSS' is for https termination at the VMSS instance VMs (using nginx https proxy). 'AppGw' is for https termination with an Azure Application Gateway. When selecting this, you need to specify all appGw* parameters. 'None' is for testing only with no https. 'None' may not be used with a separately configured https termination layer. If you want to use the 'None' option with your separately configured https termination layer, you'll need to update your Moodle config.php manually for $cfg->wwwroot and $cfg->sslproxy." }, "type": "string" }, "siteURL": { "defaultValue": "www.example.org", "metadata": { "description": "URL for Moodle site" }, "type": "string" }, "moodleVersion": { "allowedValues": [ "MOODLE_405_STABLE" ], "defaultValue": "MOODLE_405_STABLE", "metadata": { "description": "The Moodle version you want to install." }, "type": "string" }, "sshPublicKey": { "metadata": { "description": "ssh public key" }, "type": "string" }, "sshUsername": { "defaultValue": "azureadmin", "metadata": { "description": "ssh user name" }, "type": "string" }, "controllerVmSku": { "defaultValue": "Standard_DS1_v2", "metadata": { "description": "VM size for the controller VM" }, "type": "string" }, "webServerType": { "defaultValue": "nginx", "allowedValues": [ "apache", "nginx" ], "metadata": { "description": "Web server type" }, "type": "string" }, "autoscaleVmSku": { "defaultValue": "Standard_DS2_v2", "metadata": { "description": "VM size for autoscaled web VMs" }, "type": "string" }, "autoscaleVmCountMax": { "defaultValue": 10, "metadata": { "description": "Maximum number of autoscaled web VMs" }, "type": "int" }, "autoscaleVmCountMin": { "defaultValue": 1, "metadata": { "description": "Minimum (also initial) number of autoscaled web VMs" }, "type": "int" }, "osDiskStorageType": { "defaultValue": "Premium_LRS", "allowedValues": [ "Premium_LRS", "Standard_LRS" ], "metadata": { "description": "Azure storage type for all VMs' OS disks. With htmlLocalCopySwith true, Premium_LRS (SSD) is strongly recommended, as PHP files will be served from OS disks." }, "type": "string" }, "phpVersion": { "allowedValues": [ "8.1" ], "defaultValue": "8.1", "metadata": { "description": "php version" }, "type": "string" }, "dbServerType": { "defaultValue": "mysqlflex", "allowedValues": [ "postgres", "mysql", "mssql", "mysqlflex" ], "metadata": { "description": "Database type" }, "type": "string" }, "dbLogin": { "defaultValue": "dbadmin", "metadata": { "description": "Database admin username" }, "type": "string" }, "moodleDbName": { "defaultValue": "moodle", "metadata": { "description": "Moodle Database name" }, "type": "string" }, "moodleDbUser": { "defaultValue": "moodle", "metadata": { "description": "Moodle Database username. This user is different from Database admin user." }, "type": "string" }, "mysqlPgresVcores": { "allowedValues": [ 1, 2, 4, 8, 16, 32 ], "defaultValue": 2, "metadata": { "description": "MySql/Postgresql vCores. For Basic tier, only 1 & 2 are allowed. For GeneralPurpose tier, 2, 4, 8, 16, 32 are allowed. For MemoryOptimized, 2, 4, 8, 16 are allowed." }, "type": "int" }, "mysqlPgresStgSizeGB": { "defaultValue": 125, "minValue": 5, "maxValue": 1024, "metadata": { "description": "MySql/Postgresql storage size in GB. Minimum 5GB, increase by 1GB, up to 1TB (1024 GB)" }, "type": "int" }, "mysqlPgresSkuTier": { "allowedValues": [ "Basic", "GeneralPurpose", "MemoryOptimized" ], "defaultValue": "GeneralPurpose", "metadata": { "description": "MySql/Postgresql sku tier" }, "type": "string" }, "mysqlPgresSkuHwFamily": { "allowedValues": [ "Gen4", "Gen5" ], "defaultValue": "Gen5", "metadata": { "description": "MySql/Postgresql sku hardware family. Central US is Gen4 only, so make sure to change this parameter to Gen4 if your deployment is on Central US." }, "type": "string" }, "mysqlVersion": { "allowedValues": [ "8.0.21" ], "defaultValue": "8.0.21", "metadata": { "description": "Mysql version" }, "type": "string" }, "mysqlflexSkuName": { "allowedValues": [ "Standard_B1s", "Standard_B1ms", "Standard_B2s", "Standard_B2ms", "Standard_B4ms", "Standard_B8ms", "Standard_B12ms", "Standard_B16ms", "Standard_B20ms", "Standard_D2ads_v5", "Standard_D2ds_v4", "Standard_D4ads_v5", "Standard_D4ds_v4", "Standard_D8ads_v5", "Standard_D8ds_v4", "Standard_D16ads_v5", "Standard_D16ds_v4", "Standard_D32ads_v5", "Standard_D32ds_v4", "Standard_D48ads_v5", "Standard_D48ds_v4", "Standard_D64ads_v5", "Standard_D64ds_v4", "Standard_E2ds_v4", "Standard_E2ads_v5", "Standard_E4ds_v4", "Standard_E4ads_v5", "Standard_E8ds_v4", "Standard_E8ads_v5", "Standard_E16ds_v4", "Standard_E16ads_v5", "Standard_E32ds_v4", "Standard_E32ads_v5", "Standard_E48ds_v4", "Standard_E48ads_v5", "Standard_E64ds_v4", "Standard_E64ads_v5", "Standard_E80ids_v4", "Standard_E2ds_v5", "Standard_E4ds_v5", "Standard_E8ds_v5", "Standard_E16ds_v5", "Standard_E32ds_v5", "Standard_E48ds_v5", "Standard_E64ds_v5", "Standard_E96ds_v5" ], "defaultValue": "Standard_B2s", "metadata": { "description": "The name of the sku for Azure Database for MySQL Flexible Servers, e.g. Standard_D32ds_v4. Complete list is available here : https://learn.microsoft.com/en-us/azure/mysql/flexible-server/concepts-service-tiers-storage " }, "type": "string" }, "mysqlflexStgSizeGiB": { "defaultValue": 20, "minValue": 20, "maxValue": 16384, "metadata": { "description": "Azure Database for MySQL Flexible server storage size in GiB. Minimum 20GiB, increase by 1GiB, up to 16TiB (1024 GiB)" }, "type": "int" }, "mysqlflexStgIops": { "defaultValue": 360, "minValue": 360, "metadata": { "description": "Azure Database for MySQL Flexible server storage iops. Minimum 360, Maxium is determined by selected mysqlflexSkuName." }, "type": "int" }, "mysqlflexStgAutogrow": { "allowedValues": [ "Enabled", "Disabled" ], "defaultValue": "Enabled", "type": "string" }, "mysqlflexHaEnabled": { "allowedValues": [ "Disabled", "SameZone", "ZoneRedundant" ], "defaultValue": "Disabled", "metadata": { "description": "High availability mode for azure database for mysql flexible server." }, "type": "string" }, "mysqlflexAvailabilityZone": { "allowedValues": [ "1", "2", "3", "" ], "defaultValue": "", "metadata": { "description": "Availability Zone information of the server. (Leave blank for No Preference)." }, "type": "string" }, "mysqlflexStandbyAvailabilityZone": { "allowedValues": [ "1", "2", "3", "" ], "defaultValue": "", "metadata": { "description": "Availability zone of the High availability standby server. (Leave blank for No Preference). Add this value if HA is enabled." }, "type": "string" }, "postgresVersion": { "allowedValues": [ "9.6" ], "defaultValue": "9.6", "metadata": { "description": "Postgresql version" }, "type": "string" }, "sslEnforcement": { "allowedValues": [ "Disabled", "Enabled" ], "defaultValue": "Disabled", "metadata": { "description": "MySql/Postgresql SSL connection" }, "type": "string" }, "mssqlDbServiceObjectiveName": { "allowedValues": [ "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S9" ], "defaultValue": "S1", "metadata": { "description": "MS SQL database service object names" }, "type": "string" }, "mssqlDbSize": { "allowedValues": [ "100MB", "250MB", "500MB", "1GB", "2GB", "5GB", "10GB", "20GB", "30GB", "40GB", "50GB", "100GB", "250GB", "300GB", "400GB", "500GB", "750GB", "1024GB" ], "defaultValue": "250GB", "metadata": { "description": "MS SQL database size" }, "type": "string" }, "mssqlDbEdition": { "allowedValues": [ "Basic", "Standard" ], "defaultValue": "Standard", "metadata": { "description": "MS SQL DB edition" }, "type": "string" }, "mssqlVersion": { "allowedValues": [ "12.0" ], "defaultValue": "12.0", "metadata": { "description": "Mssql version" }, "type": "string" }, "fileServerType": { "defaultValue": "nfs", "allowedValues": [ "gluster", "nfs", "nfs-ha", "nfs-byo", "azurefiles" ], "metadata": { "description": "File server type: GlusterFS, NFS, and NFS-HA (2-VM highly available NFS cluster)" }, "type": "string" }, "nfsByoIpExportPath": { "defaultValue": "", "metadata": { "description": "IP address and export path of the BYO-NFS share when fileServerType == nfs-byo. E.g., 172.16.1.8:/msazure" }, "type": "string" }, "OSDiskSizeInGB": { "defaultValue": 256, "metadata": { "description": "OS disk size per Webserver in VMSS" }, "type": "int" }, "fileServerDiskSize": { "defaultValue": 127, "metadata": { "description": "Size per disk for gluster nodes or nfs server" }, "type": "int" }, "fileServerDiskCount": { "defaultValue": 4, "minValue": 2, "maxValue": 8, "metadata": { "description": "Number of disks in raid0 per gluster node or nfs server" }, "type": "int" }, "fileServerVmSku": { "defaultValue": "Standard_DS2_v2", "metadata": { "description": "VM size for the gluster or NFS-HA nodes" }, "type": "string" }, "keyVaultResourceId": { "defaultValue": "", "metadata": { "description": "(VMSS https termination only) Azure Resource Manager resource ID of the Key Vault in case you stored your SSL cert in an Azure Key Vault (Note that this Key Vault must have been pre-created on the same Azure region where this template is being deployed). Leave this blank if you didn't. Resource ID example: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/xxx/providers/Microsoft.KeyVault/vaults/yyy. This value can be obtained from keyvault.sh output if you used the script to store your SSL cert in your Key Vault." }, "type": "string" }, "sslCertKeyVaultURL": { "defaultValue": "", "metadata": { "description": "(VMSS https termination only) Azure Key Vault URL for your stored SSL cert. This value can be obtained from keyvault.sh output if you used the script to store your SSL cert in your Key Vault. This parameter is ignored if the keyVaultResourceId parameter is blank." }, "type": "string" }, "sslCertThumbprint": { "defaultValue": "", "metadata": { "description": "(VMSS https termination only) Thumbprint of your stored SSL cert. This value can be obtained from keyvault.sh output if you used the script to store your SSL cert in your Key Vault. This parameter is ignored if the keyVaultResourceId parameter is blank." }, "type": "string" }, "caCertKeyVaultURL": { "defaultValue": "", "metadata": { "description": "(VMSS https termination only) Azure Key Vault URL for your stored CA (Certificate Authority) cert. This value can be obtained from keyvault.sh output if you used the script to store your CA cert in your Key Vault. This parameter is ignored if the keyVaultResourceId parameter is blank." }, "type": "string" }, "caCertThumbprint": { "defaultValue": "", "metadata": { "description": "(VMSS https termination only) Thumbprint of your stored CA cert. This value can be obtained from keyvault.sh output if you used the script to store your CA cert in your Key Vault. This parameter is ignored if the keyVaultResourceId parameter is blank." }, "type": "string" }, "appGwSslCertKeyVaultResourceId": { "defaultValue": "", "metadata": { "description": "(App Gateway https termination only) Azure Key Vault URL for your stored SSL cert, again for App Gateway https termination case only. (Note that this Key Vault must have been pre-created on the same Azure region where this template is being deployed). Leave this blank if you didn't. Resource ID example: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/xxx/providers/Microsoft.KeyVault/vaults/yyy." }, "type": "string" }, "appGwSslCertKeyVaultSecretName": { "defaultValue": "", "metadata": { "description": "(App Gateway https termination only) Name of the Azure Key Vault secret that's stored in the previously specified Key Vault as a PFX certificate (with no password) for your site's SSL cert. This secret must be pre-populated in the specified Key Vault with the matching name." }, "type": "string" }, "appGwSkuName": { "defaultValue": "Standard_v2", "allowedValues": [ "Standard_Small", "Standard_Medium", "Standard_Large", "Standard_v2", "WAF_Medium", "WAF_Large", "WAF_v2" ], "metadata": { "description": "(App Gateway https termination only) Name of the Applicate Gateway SKU" }, "type": "string" }, "appGwSkuTier": { "defaultValue": "Standard_v2", "allowedValues": [ "Standard", "Standard_v2", "WAF", "WAF_v2" ], "metadata": { "description": "(App Gateway https termination only) Tier of the Applicate Gateway" }, "type": "string" }, "appGwSkuCapacity": { "defaultValue": 2, "maxValue": 10, "minValue": 2, "metadata": { "description": "(App Gateway https termination only) Capacity instance count) of the Applicate Gateway" }, "type": "int" }, "storageAccountType": { "defaultValue": "Standard_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type. This storage account is only for the Moodle ObjectFS plugin and/or the (currently disabled) Azure Files file share option" }, "type": "string" }, "searchType": { "defaultValue": "none", "allowedValues": [ "none", "azure", "elastic" ], "metadata": { "description": "options of moodle global search" }, "type": "string" }, "tikaService": { "defaultValue": "none", "allowedValues": [ "none", "tika" ], "metadata": { "description": "options of enabling tika service for file searching in moodle" }, "type": "string" }, "azureSearchSku": { "defaultValue": "basic", "allowedValues": [ "free", "basic", "standard", "standard2", "standard3" ], "metadata": { "description": "the search service level you want to create." }, "type": "string" }, "azureSearchReplicaCount": { "defaultValue": 3, "minValue": 1, "maxValue": 12, "metadata": { "description": "Replicas distribute search workloads across the service. You need 2 or more to support high availability (applies to Basic and Standard only)." }, "type": "int" }, "azureSearchPartitionCount": { "defaultValue": 1, "allowedValues": [ 1, 2, 3, 4, 6, 12 ], "metadata": { "description": "Partitions allow for scaling of document count as well as faster indexing by sharding your index over multiple Azure Search units." }, "type": "int" }, "azureSearchHostingMode": { "defaultValue": "default", "allowedValues": [ "default", "highDensity" ], "metadata": { "description": "Applicable only for azureSearchSku set to standard3. You can set this property to enable a single, high density partition that allows up to 1000 indexes, which is much higher than the maximum indexes allowed for any other azureSearchSku." }, "type": "string" }, "elasticVmSku": { "defaultValue": "Standard_DS2_v2", "metadata": { "description": "VM size for the elastic search nodes" }, "type": "string" }, "tikaVmSku": { "defaultValue": "Standard_DS2_v2", "metadata": { "description": "VM size for the tika search nodes" }, "type": "string" }, "customVnetId": { "defaultValue": "", "metadata": { "description": "Azure Resource ID of the Azure virtual network where you want to deploy your Moodle resources. A vnet resource ID is of the following format: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx/resourceGroups/gggg/providers/Microsoft.Network/virtualNetworks/vvvv. Note that this virtual network must be on the same Azure location as this template deployment location. If this parameter is blank, a new Azure virtual network will be created and used. In that case, the address space of the newly created virtual network will be */16 of the following vNetAddressSpace parameter value below." }, "type": "string" }, "vNetAddressSpace": { "defaultValue": "172.31.0.0", "metadata": { "description": "Address range for the Moodle virtual network and various subnets - presumed /16 for a newly created vnet in case customVnetId is blank. Further subneting (a number of */24 subnets starting from the xxx.yyy.zzz.0/24 will be created on a newly created vnet or your BYO-vnet (specified in customVnetId parameter)." }, "type": "string" }, "gatewayType": { "allowedValues": [ "Vpn", "ER" ], "defaultValue": "Vpn", "metadata": { "description": "Virtual network gateway type" }, "type": "string" }, "vpnType": { "allowedValues": [ "RouteBased", "PolicyBased" ], "defaultValue": "RouteBased", "metadata": { "description": "Virtual network gateway vpn type" }, "type": "string" }, "loadBalancerSku": { "defaultValue": "Standard", "allowedValues": [ "Basic", "Standard" ], "metadata": { "description": "Loadbalancer SKU" }, "type": "string" }, "loadBalancerTier": { "defaultValue": "Regional", "allowedValues": [ "Regional", "Global" ], "metadata": { "description": "Loadbalancer Tier" }, "type": "string" }, "ubuntuVersion": { "type": "string", "allowedValues": [ "22_04-lts-gen2" ], "defaultValue": "22_04-lts-gen2" }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "Azure Location for all resources." } }, "isMigration": { "type": "bool", "defaultValue": false, "metadata": { "description": "Indicates whether the this template is using for migration scenario." } } }, "resources": [ { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "pid-738e3eec-68d4-4667-8377-c05c77c21f1b", "properties": { "mode": "Incremental", "template": { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "resources": [] } } }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "networkTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl,'network.json',parameters('_artifactsLocationSasToken'))]" } } }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "storageAccountTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl,'storageAccount.json',parameters('_artifactsLocationSasToken'))]" } } }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/networkTemplate" ], "name": "dbTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" }, "lbPubIp": { "value": "[reference('networkTemplate').outputs.lbPubIp.value]" }, "lbOut001PubIp": { "value": "[reference('networkTemplate').outputs.lbOut001PubIp.value]" }, "lbOut002PubIp": { "value": "[reference('networkTemplate').outputs.lbOut002PubIp.value]" }, "ctlrPubIp": { "value": "[reference('networkTemplate').outputs.ctlrPubIp.value]" }, "subnetIdDb": { "value": "[reference('networkTemplate').outputs.subnetIdDb.value]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl, 'db-', parameters('dbServerType'), '.json', parameters('_artifactsLocationSasToken'))]" } } }, { "condition": "[parameters('azureBackupSwitch')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "recoveryTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl,'recoveryservices.json',parameters('_artifactsLocationSasToken'))]" } } }, { "condition": "[parameters('redisDeploySwitch')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "redisTemplate", "dependsOn": [ "Microsoft.Resources/deployments/networkTemplate" ], "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" }, "subnetIdRedis": { "value": "[reference('networkTemplate').outputs.subnetIdRedis.value]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl, 'redis.json', parameters('_artifactsLocationSasToken'))]" } } }, { "condition": "[not(equals(parameters('searchType'), 'none'))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/networkTemplate", "Microsoft.Resources/deployments/recoveryTemplate" ], "name": "searchTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" }, "subnetIdElastic": { "value": "[reference('networkTemplate').outputs.subnetIdElastic.value]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl, 'search-', parameters('searchType'), '.json', parameters('_artifactsLocationSasToken'))]" } } }, { "condition": "[and(equals(parameters('tikaService'), 'tika'), not(equals(parameters('searchType'), 'none')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/networkTemplate", "Microsoft.Resources/deployments/recoveryTemplate" ], "name": "tikaTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" }, "subnetIdTika": { "value": "[reference('networkTemplate').outputs.subnetIdTika.value]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl, 'tika.json', parameters('_artifactsLocationSasToken'))]" } } }, { "condition": "[equals(parameters('fileServerType'),'gluster')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/networkTemplate", "Microsoft.Resources/deployments/recoveryTemplate" ], "name": "glusterTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" }, "subnetIdSan": { "value": "[reference('networkTemplate').outputs.subnetIdSan.value]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl,'gluster.json',parameters('_artifactsLocationSasToken'))]" } } }, { "condition": "[equals(parameters('fileServerType'),'nfs-ha')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/networkTemplate", "Microsoft.Resources/deployments/recoveryTemplate" ], "name": "nfsHaTemplate", "properties": { "mode": "Incremental", "parameters": { "_artifactsLocation": { "value": "[parameters('_artifactsLocation')]" }, "_artifactsLocationSasToken": { "value": "[parameters('_artifactsLocationSasToken')]" }, "location": { "value": "[parameters('location')]" }, "subnetId": { "value": "[reference('networkTemplate').outputs.subnetIdSan.value]" }, "node0IPAddr": { "value": "[variables('moodleCommon').nfsHaNode0IP]" }, "node1IPAddr": { "value": "[variables('moodleCommon').nfsHaNode1IP]" }, "nfsClientsIPRange": { "value": "[variables('moodleCommon').nfsHaClientsIPRange]" }, "lbFrontEndIpAddr": { "value": "[variables('moodleCommon').nfsHaLbIP]" }, "enableAccelNwSwitch": { "value": "[parameters('enableAccelNwForOtherVmsSwitch')]" }, "vmSku": { "value": "[variables('moodleCommon').fileServerVmSku]" }, "adminUserName": { "value": "[parameters('sshUsername')]" }, "sshPublicKey": { "value": "[parameters('sshPublicKey')]" }, "osType": { "value": "[variables('moodleCommon').osType]" }, "osDiskStorageType": { "value": "[parameters('osDiskStorageType')]" }, "dataDiskCountPerVM": { "value": "[parameters('fileServerDiskCount')]" }, "dataDiskSizeInGB": { "value": "[parameters('fileServerDiskSize')]" }, "OSDiskSizeInGB": { "value": "[parameters('OSDiskSizeInGB')]" }, "resourcesUniqueString": { "value": "[variables('resourceprefix')]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl, 'nfs-ha.json', parameters('_artifactsLocationSasToken'))]" } } }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/networkTemplate", "Microsoft.Resources/deployments/dbTemplate", "Microsoft.Resources/deployments/redisTemplate", "Microsoft.Resources/deployments/searchTemplate", "Microsoft.Resources/deployments/storageAccountTemplate" ], "name": "vmSetupParamsTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" }, "dbFQDN": { "value": "[reference('dbTemplate').outputs.dbFQDN.value]" }, "storageAccountName": { "value": "[reference('storageAccountTemplate').outputs.storageAccountName.value]" }, "storageAccountKey": { "value": "[reference('storageAccountTemplate').outputs.storageAccountKey.value]" }, "redisKey": { "value": "[if(parameters('redisDeploySwitch'), reference('redisTemplate').outputs.redisKey.value, 'None')]" }, "azureSearchKey": { "value": "[if(equals(parameters('searchType'), 'azure'), reference('searchTemplate').outputs.azureSearchKey.value, 'None')]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl,'vmsetupparams.json',parameters('_artifactsLocationSasToken'))]" } } }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/vmSetupParamsTemplate", "Microsoft.Resources/deployments/glusterTemplate", "Microsoft.Resources/deployments/nfsHaTemplate", "Microsoft.Resources/deployments/recoveryTemplate", "Microsoft.Resources/deployments/networkTemplate", "Microsoft.Resources/deployments/dbTemplate", "Microsoft.Resources/deployments/redisTemplate", "Microsoft.Resources/deployments/searchTemplate", "Microsoft.Resources/deployments/storageAccountTemplate" ], "name": "controllerTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" }, "subnetIdWeb": { "value": "[reference('networkTemplate').outputs.subnetIdWeb.value]" }, "ctlrPubIpId": { "value": "[reference('networkTemplate').outputs.ctlrPubIpId.value]" }, "vmSetupParamsObj": { "value": "[reference('vmSetupParamsTemplate').outputs.vmSetupParamsObj.value]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl,'controller.json',parameters('_artifactsLocationSasToken'))]" } } }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/vmSetupParamsTemplate", "Microsoft.Resources/deployments/controllerTemplate", "Microsoft.Resources/deployments/networkTemplate", "Microsoft.Resources/deployments/redisTemplate", "Microsoft.Resources/deployments/dbTemplate" ], "name": "scaleSetTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" }, "subnetIdWeb": { "value": "[reference('networkTemplate').outputs.subnetIdWeb.value]" }, "vmSetupParamsObj": { "value": "[reference('vmSetupParamsTemplate').outputs.vmSetupParamsObj.value]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl,'webvmss.json',parameters('_artifactsLocationSasToken'))]" } } } ], "outputs": { "siteURL": { "type": "string", "value": "[variables('moodleCommon').siteURL]" }, "controllerInstanceIP": { "type": "string", "value": "[reference('controllerTemplate').outputs.controllerIP.value]" }, "databaseDNS": { "type": "string", "value": "[reference('dbTemplate').outputs.dbFQDN.value]" }, "databaseAdminUsername": { "type": "string", "value": "[variables('moodleCommon').dbUsername]" }, "databaseAdminPassword": { "type": "string", "value": "[variables('moodleCommon').dbLoginPassword]" }, "firstFrontendVmIP": { "type": "string", "value": "[reference('scaleSetTemplate').outputs.webvm1IP.value]" }, "moodleAdminPassword": { "type": "string", "value": "[variables('moodleCommon').moodleAdminPass]" }, "moodleDbUsername": { "type": "string", "value": "[variables('moodleCommon').moodleDbUserAzure]" }, "moodleDbPassword": { "type": "string", "value": "[variables('moodleCommon').moodleDbPass]" }, "loadBalancerDNS": { "type": "string", "value": "[variables('moodleCommon').lbDns]" }, "loadBalancerSku": { "type": "string", "value": "[variables('moodleCommon').lbSku]" }, "loadBalancerTier": { "type": "string", "value": "[variables('moodleCommon').lbTier]" } }, "variables": { "documentation01": "This main-template calls multiple sub-templates to create the moodle system", "documentation02": " recoveryservices0 - dummy template (see next statement)", "documentation03": " recoveryservices1 - creates a recovery vault that will be subsequently used by the VM Backup - a paramter swtich controls whethe is is called or bypassed", "documentation04": " redis - creates a redis cache", "documentation05": " postgres / mysql - creates a postgresql / mysql server", "documentation06": " vnet - creates a virtual network with three subnets", "documentation07": " elastic - creates a elastic search cluster on a vm farm", "documentation08": " gluster - creates a gluster file system on a vm farm", "documentation09": " webvmss - creates a vm scale set", "documentation10": " controller - creates a controller VM and deploys code", "documentation11": "GlusterFS Sizing guidance", "moodleCommon": { "baseTemplateUrl": "[concat(parameters('_artifactsLocation'), 'nested/')]", "scriptLocation": "[concat(parameters('_artifactsLocation'), 'scripts/')]", "artifactsSasToken": "[parameters('_artifactsLocationSasToken')]", "appGwBePoolName": "[concat('appgw-bepool-', variables('resourceprefix'))]", "appGwName": "[concat('appgw-', variables('resourceprefix'))]", "appGwPipName": "[concat('appgw-pubip-',variables('resourceprefix'))]", "appGwSslCertKeyVaultResourceId": "[parameters('appGwSslCertKeyVaultResourceId')]", "appGwSslCertKeyVaultSecretName": "[parameters('appGwSslCertKeyVaultSecretName')]", "appGwSkuCapacity": "[parameters('appGwSkuCapacity')]", "appGwSkuName": "[parameters('appGwSkuName')]", "appGwSkuTier": "[parameters('appGwSkuTier')]", "applyScriptsSwitch": "[parameters('applyScriptsSwitch')]", "autoscaleVmCountMax": "[parameters('autoscaleVmCountMax')]", "autoscaleVmCountMin": "[parameters('autoscaleVmCountMin')]", "autoscaleVmSku": "[parameters('autoscaleVmSku')]", "azureBackupSwitch": "[parameters('azureBackupSwitch')]", "azureSearchHostingMode": "[parameters('azureSearchHostingMode')]", "azureSearchName": "[concat('azure-search-',variables('resourceprefix'))]", "azureSearchNameHost": "[concat('azure-search-',variables('resourceprefix'),'.search.windows.net')]", "azureSearchPartitionCount": "[parameters('azureSearchPartitionCount')]", "azureSearchReplicaCount": "[parameters('azureSearchReplicaCount')]", "azureSearchSku": "[parameters('azureSearchSku')]", "commonFunctionsScriptUri": "[concat(parameters('_artifactsLocation'),'scripts/helper_functions.sh',parameters('_artifactsLocationSasToken'))]", "controllerVmSku": "[parameters('controllerVmSku')]", "customVnetId": "[parameters('customVnetId')]", "ctlrNicName": "[concat('controller-vm-nic-',variables('resourceprefix'))]", "ctlrNsgName": "[concat('controller-nsg-',variables('resourceprefix'))]", "ctlrPipName": "[concat('controller-pubip-',variables('resourceprefix'))]", "ctlrVmName": "[concat('controller-vm-',variables('resourceprefix'))]", "vmssNsgName": "[concat('vmss-nsg-',variables('resourceprefix'))]", "ctlrVmSecrets": "[take(variables('ctlrVmSecretsArray'), if(empty(parameters('keyVaultResourceId')), 0, 1))]", "dbLogin": "[parameters('dbLogin')]", "dbLoginPassword": "[concat(substring(uniqueString(resourceGroup().id, deployment().name), 2, 11), '*7', toUpper('pfiwb'))]", "dbServerType": "[parameters('dbServerType')]", "dbUsername": "[if(equals(parameters('dbServerType'), 'mysqlflex'), parameters('dbLogin'), concat(parameters('dbLogin'), '@', parameters('dbServerType'), '-', variables('resourceprefix')))]", "ddosPlanName": "[concat('ddos-plan-',variables('resourceprefix'))]", "ddosSwitch": "[parameters('ddosSwitch')]", "elasticVmSku": "[parameters('elasticVmSku')]", "elasticAvailabilitySetName": "[concat('elastic-avset-',variables('resourceprefix'))]", "elasticClusterName": "[concat('es-cluster-',variables('resourceprefix'))]", "elasticNicName1": "[concat('elastic-vm-nic-01-',variables('resourceprefix'))]", "elasticNicName2": "[concat('elastic-vm-nic-02-',variables('resourceprefix'))]", "elasticNicName3": "[concat('elastic-vm-nic-03-',variables('resourceprefix'))]", "elasticScriptFilename": "install_elastic.sh", "elasticVm1IP": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),5)), '.20')]", "elasticVm2IP": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),5)), '.21')]", "elasticVm3IP": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),5)), '.22')]", "elasticVmName": "[concat('elastic-vm-',variables('resourceprefix'))]", "elasticVmName1": "[concat('elastic-vm-01-',variables('resourceprefix'))]", "elasticVmName2": "[concat('elastic-vm-02-',variables('resourceprefix'))]", "elasticVmName3": "[concat('elastic-vm-03-',variables('resourceprefix'))]", "enableAccelNwForCtlrVmSwitch": "[parameters('enableAccelNwForCtlrVmSwitch')]", "enableAccelNwForOtherVmsSwitch": "[parameters('enableAccelNwForOtherVmsSwitch')]", "extBeName": "[concat('lb-backend-',variables('resourceprefix'))]", "extFeName": "[concat('lb-frontend-',variables('resourceprefix'))]", "extOutName001": "[concat('lb-outbound001-',variables('resourceprefix'))]", "extOutName002": "[concat('lb-outbound002-',variables('resourceprefix'))]", "extNatPool": "[concat('lb-natpool-',variables('resourceprefix'))]", "extProbeHTTP": "[concat('lb-probe-http-',variables('resourceprefix'))]", "extProbeHTTPS": "[concat('lb-probe-https-',variables('resourceprefix'))]", "fileServerDiskCount": "[parameters('fileServerDiskCount')]", "fileServerDiskSize": "[parameters('fileServerDiskSize')]", "OSDiskSizeInGB": "[parameters('OSDiskSizeInGB')]", "fileServerType": "[parameters('fileServerType')]", "fileServerVmCount": 2, "fileServerVmSku": "[parameters('fileServerVmSku')]", "gatewayName": "[concat('vnet-gateway-',variables('resourceprefix'))]", "gatewayPublicIPName": "[concat('vnet-gw-ip-',variables('resourceprefix'))]", "gatewayType": "[parameters('gatewayType')]", "gfsNameRoot": "[concat('gluster-vm-',variables('resourceprefix'))]", "gfxAvailabilitySetName": "[concat('gluster-avset-',variables('resourceprefix'))]", "glusterScriptFilename": "install_gluster.sh", "htmlLocalCopySwitch": "[parameters('htmlLocalCopySwitch')]", "httpsTermination": "[parameters('httpsTermination')]", "installGdprPluginsSwitch": "[parameters('installGdprPluginsSwitch')]", "installO365pluginsSwitch": "[parameters('installO365pluginsSwitch')]", "installObjectFsSwitch": "[parameters('installObjectFsSwitch')]", "lbDns": "[concat('lb-',variables('resourceprefix'),'.',parameters('location'),'.cloudapp.azure.com')]", "lbSku": "[parameters('loadBalancerSku')]", "lbTier": "[parameters('loadBalancerTier')]", "lbName": "[concat('lb-',variables('resourceprefix'))]", "lbOutName001": "[concat('lb-out001-',variables('resourceprefix'))]", "lbOutName002": "[concat('lb-out002-',variables('resourceprefix'))]", "lbPipName": "[concat('lb-pubip-',variables('resourceprefix'))]", "lbOutPipName001": "[concat('lb-outpubip001-',variables('resourceprefix'))]", "lbOutPipName002": "[concat('lb-outpubip002-',variables('resourceprefix'))]", "location": "[parameters('location')]", "moodleAdminPass": "[concat(toUpper('xl'), substring(uniqueString(resourceGroup().id, deployment().name), 6, 7),',1*8')]", "moodleDbName": "[parameters('moodleDbName')]", "moodleDbPass": "[concat('9#36^', substring(uniqueString(resourceGroup().id, deployment().name), 5, 8), toUpper('ercq'))]", "moodleDbUser": "[parameters('moodleDbUser')]", "moodleDbUserAzure": "[if(equals(parameters('dbServerType'), 'mysqlflex'), parameters('moodleDbUser'), concat(parameters('moodleDbUser'), '@', parameters('dbServerType'), '-', variables('resourceprefix')))]", "moodleInstallScriptFilename": "install_moodle.sh", "moodleOnAzureConfigsJsonPath": "/var/lib/cloud/instance/moodle_on_azure_configs.json", "moodleVersion": "[parameters('moodleVersion')]", "mssqlDbServiceObjectiveName": "[parameters('mssqlDbServiceObjectiveName')]", "mssqlDbSize": "[parameters('mssqlDbSize')]", "mssqlDbEdition": "[parameters('mssqlDbEdition')]", "mssqlVersion": "[parameters('mssqlVersion')]", "mysqlPgresSkuHwFamily": "[parameters('mysqlPgresSkuHwFamily')]", "mysqlPgresSkuName": "[concat(if(equals(parameters('mysqlPgresSkuTier'),'Basic'),'B', if(equals(parameters('mysqlPgresSkuTier'),'GeneralPurpose'),'GP', 'MO')), '_', parameters('mysqlPgresSkuHwFamily'), '_', string(parameters('mysqlPgresVcores')))]", "mysqlPgresSkuTier": "[parameters('mysqlPgresSkuTier')]", "mysqlPgresStgSizeGB": "[parameters('mysqlPgresStgSizeGB')]", "mysqlPgresVcores": "[parameters('mysqlPgresVcores')]", "mysqlVersion": "[parameters('mysqlVersion')]", "mysqlflexSkuName": "[parameters('mysqlflexSkuName')]", "mysqlflexSkuTier": "[if(startsWith(parameters('mysqlflexSkuName'), 'Standard_B'), 'Burstable', if(startsWith(parameters('mysqlflexSkuName'), 'Standard_D'), 'GeneralPurpose', 'MemoryOptimized'))]", "mysqlflexStgSizeGiB": "[parameters('mysqlflexStgSizeGiB')]", "mysqlflexStgIops": "[parameters('mysqlflexStgIops')]", "mysqlflexStgAutogrow": "[parameters('mysqlflexStgAutogrow')]", "mysqlflexHaEnabled": "[parameters('mysqlflexHaEnabled')]", "mysqlflexAvailabilityZone": "[parameters('mysqlflexAvailabilityZone')]", "mysqlflexStandbyAvailabilityZone": "[parameters('mysqlflexStandbyAvailabilityZone')]", "mysqlflexRequireSecureTransport": "[if(equals(parameters('sslEnforcement'),'Enabled'),'ON','OFF')]", "mysqlflexPrivateDnsZoneName": "[concat(parameters('dbServerType'), '-',variables('resourceprefix'),'.private.mysql.database.azure.com')]", "nfsByoIpExportPath": "[parameters('nfsByoIpExportPath')]", "nfsHaClientsIPRange": "[variables('subnetWebRange')]", "nfsHaExportPath": "/drbd/data", "nfsHaLbIP": "[concat(variables('subnetSanPrefix'), '.100')]", "nfsHaNode0IP": "[concat(variables('subnetSanPrefix'), '.110')]", "nfsHaNode1IP": "[concat(variables('subnetSanPrefix'), '.120')]", "osDiskStorageType": "[parameters('osDiskStorageType')]", "osType": { "offer": "0001-com-ubuntu-server-jammy", "publisher": "Canonical", "sku": "[parameters('ubuntuVersion')]", "version": "latest" }, "phpVersion": "[parameters('phpVersion')]", "policyName": "[concat('policy-',variables('resourceprefix'))]", "postgresVersion": "[parameters('postgresVersion')]", "redisCacheName": "[concat('redis-',variables('resourceprefix'))]", "redisDeploySwitch": "[parameters('redisDeploySwitch')]", "redisDns": "[concat('redis-',variables('resourceprefix'),'.redis.cache.windows.net')]", "resourcesPrefix": "[variables('resourceprefix')]", "searchType": "[parameters('searchType')]", "serverName": "[concat(parameters('dbServerType'), '-',variables('resourceprefix'))]", "siteURL": "[if(or(empty(parameters('siteURL')), equals(parameters('siteURL'), 'www.example.org')), concat(if(equals(parameters('httpsTermination'), 'AppGw'),'appgw-','lb-'),variables('resourceprefix'),'.',parameters('location'),'.cloudapp.azure.com'), parameters('siteURL'))]", "sshPublicKey": "[parameters('sshPublicKey')]", "sshUsername": "[parameters('sshUsername')]", "sslEnforcement": "[parameters('sslEnforcement')]", "storageAccountName": "[tolower(concat('abs',variables('resourceprefix')))]", "storageAccountType": "[parameters('storageAccountType')]", "subnetDb": "[concat('db-subnet-',variables('resourceprefix'))]", "subnetDbPrefix": "[concat(variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),7)))]", "subnetDbRange": "[concat(variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),7)), '.0/24')]", "subnetAppGw": "[concat('appgw-subnet-',variables('resourceprefix'))]", "subnetAppGwPrefix": "[concat(variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),6)))]", "subnetAppGwRange": "[concat(variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),6)), '.0/24')]", "subnetElastic": "[concat('elastic-subnet-',variables('resourceprefix'))]", "subnetElasticPrefix": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),5)))]", "subnetElasticRange": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),5)), '.0/24')]", "subnetGateway": "GatewaySubnet", "subnetGatewayPrefix": "[concat(variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),4)))]", "subnetGatewayRange": "[concat(variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),4)), '.0/24')]", "subnetRedis": "[concat('redis-subnet-',variables('resourceprefix'))]", "subnetRedisPrefix": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),3)))]", "subnetRedisRange": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),3)), '.0/24')]", "subnetSan": "[concat('san-subnet-',variables('resourceprefix'))]", "subnetSanPrefix": "[variables('subnetSanPrefix')]", "subnetSanRange": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),2)), '.0/24')]", "subnetTika": "[concat('tika-subnet-',variables('resourceprefix'))]", "subnetTikaPrefix": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),1)))]", "subnetTikaRange": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),1)), '.0/24')]", "subnetWeb": "[concat('web-subnet-',variables('resourceprefix'))]", "subnetWebPrefix": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),0)))]", "subnetWebRange": "[variables('subnetWebRange')]", "thumbprintSslCert": "[if(or(empty(parameters('keyVaultResourceId')), empty(parameters('sslCertThumbprint'))), 'None', parameters('sslCertThumbprint'))]", "thumbprintCaCert": "[if(or(empty(parameters('keyVaultResourceId')), empty(parameters('caCertThumbprint'))), 'None', parameters('caCertThumbprint'))]", "tikaNicName": "[concat('tika-vm-nic-',variables('resourceprefix'))]", "tikaScriptFilename": "install_tika.sh", "tikaService": "[parameters('tikaService')]", "tikaVmIP": "[if(equals(parameters('tikaService'), 'tika'), concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),1)), '.20'), 'none')]", "tikaVmName": "[concat('tika-vm-',variables('resourceprefix'))]", "tikaVmSku": "[parameters('tikaVmSku')]", "vNetAddressSpace": "[parameters('vNetAddressSpace')]", "vaultName": "[concat('vault-',variables('resourceprefix'))]", "vmssName": "[concat('vmss-',variables('resourceprefix'))]", "vmssdStorageAccounttName": "[concat('vmss',uniqueString(resourceGroup().id))]", "vnetDbDeploySwitch": "[and(parameters('vnetDbDeploySwitch'), equals(parameters('dbServerType'), 'mysqlflex'))]", "vnetGwDeploySwitch": "[parameters('vnetGwDeploySwitch')]", "vnetName": "[concat('vnet-',variables('resourceprefix'))]", "vpnType": "[parameters('vpnType')]", "webServerSetupScriptFilename": "setup_webserver.sh", "webServerType": "[parameters('webServerType')]", "isMigration": "[parameters('isMigration')]" }, "certUrlArray": [ { "certificateUrl": "[parameters('sslCertKeyVaultURL')]" }, { "certificateUrl": "[parameters('caCertKeyVaultURL')]" } ], "ctlrVmSecretsArray": [ { "sourceVault": { "id": "[parameters('keyVaultResourceId')]" }, "vaultCertificates": "[take(variables('certUrlArray'), if(empty(parameters('caCertKeyVaultURL')), 1, 2))]" } ], "octets": "[split(parameters('vNetAddressSpace'), '.')]", "resourceprefix": "[substring(uniqueString(resourceGroup().id, deployment().name), 3, 6)]", "subnetSanPrefix": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),2)))]", "subnetWebRange": "[concat( variables('octets')[0], '.', variables('octets')[1], '.', string(add(int(variables('octets')[2]),0)), '.0/24')]" } } ================================================ FILE: azuredeploy.parameters.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "sshPublicKey": { "value": "GEN-SSH-PUB-KEY" }, "fileServerDiskCount": { "value": 2 }, "fileServerDiskSize": { "value": 32 } } } ================================================ FILE: docs/Cleanup.md ================================================ # Cleanup All Resources To cleanup a Moodle deployment simply delete the Resource Group that contains it. The commands below will iterate over your workspace directory and delete all deployments. ## Prerequisites First we need to ensure our [environment variables](./Environment-Variables.md) are correctly configured. ## Remove each resource group This command will delete all resources in *all* resource groups. Run with caution. Note, that this command will not fully delete the resource group if you have Azure Backup enabled since the Recovery Services Vault will not be deleted (it's got the backups of you data!). ``` bash for filename in $MOODLE_AZURE_WORKSPACE/*; do az group delete --yes --name $(basename $filename) --no-wait; done ``` ================================================ FILE: docs/Deploy.md ================================================ # Deploy Autoscaling Moodle Stack to Azure After following the steps in this this document you with awill have a new Moodle site with caching for speed and scaling frontends to handle load. The filesystem behind it is mirrored for high availability and optionally backed up through Azure. Filesystem permissions and options have also been tuned to make Moodle more secure than a default install. ## Prerequisites To make things consitent across different sessions managing Moodle we should [configure the environment](./Preparation.md). ## Create Resource Group When you create the Moodle cluster you will create many resources. On Azure it is a best practice to collect such resources together in a Resource Group. The first thing we need to do, therefore, is create a resource group: ``` az group create --name $MOODLE_RG_NAME --location $MOODLE_RG_LOCATION ``` Results: ```expected_similarity=0.4 { "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/rgmoodlearm3", "location": "westus2", "managedBy": null, "name": "rgmoodlearm3", "properties": { "provisioningState": "Succeeded" }, "tags": null } ``` ## Create Azure Deployment Parameters Your deployment will be configured using an `azuredeploy.parameters.json` file. It is possible to provide these parameters interactively via the command line by simply omitting the paramaters file in the command in the next section. However, it is more reproducible if we use a paramaters file. A good set of defaults are provided in the git repository. These defaults create a scalable cluster that is suitable for low volume testing. If you are building out a production service you should review the section below on sizing considerations. For now we will proceed with the defaults, but there is one value, the `sshPublicKey` that **must** be provided. The following command will replace the placeholder in the parameters template file with an SSH key used for testing puporses (this is created as part of the envrionment setup in the prerequisites): ``` bash ssh_pub_key=`cat $MOODLE_SSH_KEY_FILENAME.pub` echo $ssh_pub_key sed "s|GEN-SSH-PUB-KEY|$ssh_pub_key|g" $MOODLE_AZURE_WORKSPACE/arm_template/azuredeploy.parameters.json > $MOODLE_AZURE_WORKSPACE/$MOODLE_RG_NAME/azuredeploy.parameters.json ``` If you'd like to configure the Moodle cluster (to be deployed) with your own SSL certificate for your domain (siteURL) at the deployment time, you can do so by using [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) and following the instructions in the [SSL cert documentation](SslCert.md). For more information see the [parameters documentation](Parameters.md). ## Deploy cluster Now that we have a resource group and a configuration file we can create the cluster itself. This is done with a single command: ``` az deployment group create --name $MOODLE_DEPLOYMENT_NAME --resource-group $MOODLE_RG_NAME --template-file $MOODLE_AZURE_WORKSPACE/arm_template/azuredeploy.json --parameters $MOODLE_AZURE_WORKSPACE/$MOODLE_RG_NAME/azuredeploy.parameters.json ``` ## Using the created stack In testing, stacks typically took between 1 and 2 hours to finish, depending on spec. Once complete you will receive a JSON output containing information needed to manage your Moodle install (see `outputs`). You can also retrieve this infromation from the portal or the CLI. Once Moodle has been created, and (where necessary) you have configured your custom `siteURL` DNS to point to the `loadBalancerDNS`, you should be able to load the `siteURL` in a browser and login with the username "admin" and the `moodleAdminPassword`. Note that the values for each of these parameters are avialble in the portal or the `outputs` section of the JSON response from the previous deploy command. See [documentation on how to retrieve configuration data](./Get-Install-Data.md) along with full details of all the output parameters avialble to you. Note that by default the deployment uses a self-signed certificate, consequently you will recieve a warning when accessing the site. To add a genuine certificate see the documentation on [managing your cluster](./Manage.md). ## Sizing Considerations and Limitations Depending on what you're doing with Moodle you will want to configure your deployment appropriately.The defaults included produce a cluster that is inexpensive but probably too low spec to use beyond simple testing scenarios. This section includes an overview of how to size the database and VM instances for your use case. ### Database Sizing As of the time of this writing, Azure supports "Basic", "General Purpose" and "Memory Optimized" tiers for MySQL/PostgreSQL database instances. In addition the mysqlPgresVcores defines the number of vCores for each DB server instance, and the number of those you can use is limited by database tier: - Basic: 1, 2 - General Purpose: 2, 4, 8, 16, 32 - Memory Optimized: 2, 4, 8, 16 This value also limits the maximum number of connections, as defined here: https://docs.microsoft.com/en-us/azure/mysql/concepts-limits As the Moodle database will handle cron processes as well as the website, any public facing website with more than 10 users will likely require upgrading to 100. Once the site reaches 30+ users it will require upgrading to General Purpose for more compute units. This depends entirely on the individual site. As MySQL databases cannot change (or be restored to a different tier) once deployed it is a good idea to slightly overspec your database. All MySQL/PostgreSQL database storage, regardless of tier, has a hard upper limit of 1 terabyte (1024 GB), starting from 5 GB minimum, increasing by 1 GB. You gain additional iops for each added GB, so if you're expecting a heavy amount of traffic you will want to oversize your storage. The current maximum iops with a 1TB disk is 3000. ### Controller instance sizing The controller handles both syslog and cron duties. Depending on how big your Moodle cron runs are this may not be sufficient. If cron jobs are very delayed and cron processes are building up on the controller then an upgrade in tier is needed. ### Frontend instances In general the frontend instances will not be the source of any bottlenecks unless they are severely undersized versus the rest of the cluster. More powerful instances will be needed should fpm processes spawn and exhaust memory during periods of heavy site load. This can also be mitigated against by increasing the number of VMs but spawning new VMs is slower (and potentially more expensive) than having that capacity already available. It is worth noting that the memory allowances on these instances allow for more memory than they may be able to provide with lower instance tiers. This is intentional as you can opt to run larger VMs with more memory and not require manual configuration. FPM also allows for a very large number of threads which prevents the system from failing during many small jobs. ## Next Steps 1. [Retrieve configuration details using CLI](./Get-Install-Data.md) 1. [Manage the Moodle cluster](./Manage.md) ================================================ FILE: docs/Environment-Variables.md ================================================ # Environment Variables In order to configure our deployment and tools we'll set up some environment variables to ensure consistency. If you are running these scripts through SimDem you can customize these values by copying and editing `env.json` into `env.local.json`. We'll need a unique name for our Resource Group in Azure, but when running in an automated mode it is useful to have a (mostly) unique name for your deployment and related resources. We'll use a timestamp. If the environmnt variable `MOODLE_RG_NAME` is not set we will create a new value using a timestamp: ``` shell if [ -z "$MOODLE_RG_NAME" ]; then MOODLE_RG_NAME=moodle_$(date +%Y-%m-%d-%H); fi ``` Other configurable values for our Azure deployment include the location and depoloyment name. We'll standardize these, but you can use different values if you like. ``` shell MOODLE_RG_LOCATION=southcentralus MOODLE_DEPLOYMENT_NAME=MasterDeploy ``` We also need to provide an SSH key. Later we'll generate this if it doesn't already exist but to enable us to reuse an existing key we'll store it's filename in an Environment Variable. ``` shell MOODLE_SSH_KEY_FILENAME=~/.ssh/moodle_id_rsa ``` We need a workspace for storing configuration files and other per-deployment artifacts: ``` shell MOODLE_AZURE_WORKSPACE=~/.moodle ``` ## Create Workspace Ensure the workspace for this particular deployment exists: ``` mkdir -p $MOODLE_AZURE_WORKSPACE/$MOODLE_RG_NAME ``` ## Validation After working through this file there should be a number of environment variables defined that will be used to provide a common setup for all our Moodle on Azure work. The resource group name defines the name of the group into which all resources will be, or are, deployed. ```bash echo "Resource Group for deployment: $MOODLE_RG_NAME" ``` Results: ``` Resource Group for deployment: southcentralus ``` The resource group location is: ```bash echo "Deployment location: $MOODLE_RG_LOCATION" ``` Results: ``` Deployment location: southcentralus ``` When deploying a Moodle cluster the deployment will be given a name so that it can be identified later should it be neceessary to debug. ```bash echo "Deployment name: $MOODLE_DEPLOYMENT_NAME" ``` Results: ``` Deployment name: MasterDeploy ``` The SSH key to use can be found in a file, if necessary this will be created as part of these scripts. ``` shell echo "SSH key filename: $MOODLE_SSH_KEY_FILENAME" ``` Results: ``` SSH key filename: ~/.ssh/moodle_id_rsa ``` Configuration files will be written to / read from a customer directory: ``` shell echo "Workspace directory: $MOODLE_AZURE_WORKSPACE" ``` Results: ``` Workspace directory: ~/.moodle ``` Ensure the workspace directory exists: ``` bash if [ ! -f "$MOODLE_AZURE_WORKSPACE/$MOODLE_RG_NAME" ]; then echo "Worspace exists"; fi ``` Results: ``` Workspace exists ``` ================================================ FILE: docs/Get-Install-Data.md ================================================ # Retrieve essential install details Once a deployment has completed the ARM template will output some values that you will need for managing your Moodle instalation. These are available in the portal, but in this document we will retrieve them using the AZ command line tools and through the AZ CLI tool. This document describes the available parameters and how to retrieve them. ## Prerequisites In order to configure our deployment and tools we'll set up some [environment variables](./Environment-Variables.md) to ensure consistency. ## Output Paramater Overview The available output parameters are: - **siteURL**: If you provided a `siteURL` parameter when deploying this will be set to the supplied value. Otherwise it will be the same as the loadBalancerDNS, see below. - **loadBalancerDNS**: This is the DNS name of your application load balancer. If you provided a `siteURL` parameter when deploying you'll need to add a DNS entry to its CNAMEs pointing to this address. - **moodleAdminPassword**: The generated password for the "admin" user in your Moodle install. - **controllerInstanceIP**: This is the IP address of the controller Virtual Machine. You will need to SSH into this to make changes to your Moodle code or view logs. - **databaseDNS**: This is the public DNS of your database instance. If you wish to set up local backups or access the DB directly, you'll need to use this. - **databaseAdminUsername**: The admin username for your database (this is not the same as your Moodle username). - **databaseAdminPassword**: The admin password for your database (this is not the same as your Moodle password). ## Retrieving Output Parameters Using the CLI To get a complete list of outputs in json format use: ```bash az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out json --query *.outputs ``` Individual outputs can be retrieved by filtering, for example, to get just the value of the `siteURL` use: ``` bash az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out json --query *.outputs.siteURL.value ``` However, since we are reqeusting JSON output (the default) the value is enclosed in quotes. In order to remove these we can output as a tab separated list (TSV): ``` bash az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.siteURL ``` Now we can assign individual values to environment variables, for example: ``` bash MOODLE_SITE_URL="$(az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.siteURL.value)" ``` ### Retrieving Moodle Site URL The Site URL is the value used to configure Moodle's base URL. The site URL can be provided as an input to the template via the parameter `siteURL`, in which case you will not need to retrieve this from the outputs. However, if you do not define this, or if you leave it as the default "www.example.org" you will need to retrieve this value from Azure using the following command: ```bash MOODLE_SITE_URL="$(az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.siteURL.value)" ``` #### Retrieving Moodle Site Load Balancer URL The load balancer DNS is the publicly registered DNS name for your Moodle DNS. If this is different from the site URL it is important to ensure that you configure your DNS entry for site URL to point at the load balancer. ```bash MOODLE_LOAD_BALANCER_DNS="$(az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.loadBalancerDNS.value)" ``` ### Retrieving Moodle Administrator Password Moodle admin password (username is "admin"): ```bash MOODLE_ADMIN_PASSWORD="$(az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.moodleAdminPassword.value)" ``` ### Retriving Controller Virtual Machine Details The controller VM runs management tasks for the cluster, such as cron jobs and syslog. ```bash MOODLE_CONTROLLER_INSTANCE_IP="$(az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.controllerInstanceIP.value)" ``` There is no username and password for this VM since a username and SSH key are provided as input parameters to the template. ### Retreiving Database Information #### Database URL ``` bash MOODLE_DATABASE_DNS="$(az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.databaseDNS.value)" ``` #### Database admin username ``` bash MOODLE_DATABASE_ADMIN_USERNAME="$(az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.databaseAdminUsername.value)" ``` #### Database admin password ``` bash MOODLE_DATABASE_ADMIN_PASSWORD="$(az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.databaseAdminPassword.value)" ``` ### Retrieving Moodle Application VNET Information First frontend VM IP: ``` bash MOODLE_FIRST_FRONTEND_VM_IP="$(az deployment group show --resource-group $MOODLE_RG_NAME --name $MOODLE_DEPLOYMENT_NAME --out tsv --query *.outputs.firstFrontendVmIP.value)" ``` # Validation After having run each of the commands in this document you should have each of the output parameters available in environment variable: ``` bash echo $MOODLE_SITE_URL echo $MOODLE_LOAD_BALANCER_DNS echo $MOODLE_ADMIN_PASSWORD echo $MOODLE_CONTROLLER_INSTANCE_IP echo $MOODLE_DATABASE_DNS echo $MOODLE_DATABASE_ADMIN_USERNAME echo $MOODLE_DATABASE_ADMIN_PASSWORD echo $MOODLE_FIRST_FRONTEND_VM_IP ``` ## Next Steps 1. [Manage the Moodle cluster](./Manage.md) ================================================ FILE: docs/Manage.md ================================================ # Managing a Scalable Moodle Cluster in Azure This document provides an overview of how to perform various management tasks on a scalable Moodle cluster on Azure. ## Prerequisites In order to configure our deployment and tools we'll set up some [environment variables](./Environment-Variables.md) to ensure consistency. In order to manage a cluster it is clearly necessary to first [deploy a scalable Moodle cluster on Azure](./Deploy.md). For convenience and readability this document also assumes that essential [deployment details for your cluster have been assigned to environment variables](./Get-Install-Data.md). ## Updating Moodle code/settings Your controller Virtual Machine has Moodle code and data stored in `/moodle`. The site code is stored in `/moodle/html/moodle/`. This data is replicated across dual gluster nodes to provide high availability. This directory is also mounted to your autoscaled frontends so all changes to files on the controller VM are immediately available to all frontend machines (when the `htmlLocalCopySwitch` in `azuredeploy.json` is false--otherwise, see below). Note that any updates on Moodle code/settings (e.g., additional plugin installations, Moodle version upgrade) have to be done on the controller VM using shell commands, not through a web browser, because the HTML directory's permission is read-only for the web frontend VMs (thus any web-based Moodle code updates will fail). Depending on how large your Gluster disks are sized, it may be helpful to keep multiple older versions (/moodle/html1, /moodle/html2, etc) to roll back if needed. To connect to your Controller VM use SSH with a username of 'azureuser' and the SSH provided in the `sshPublicKey` input parameter. For example, to retrieve a listing of files and directories in the `/moodle` directory use: ``` ssh -o StrictHostKeyChecking=no azureadmin@$MOODLE_CONTROLLER_INSTANCE_IP ls -l /moodle ``` Results: ``` Warning: Permanently added '52.228.45.38' (ECDSA) to the list of known hosts. total 12 drwxr-xr-x 2 www-data www-data 4096 Jan 17 00:59 certs -rw-r--r-- 1 root root 0 Jan 17 02:22 db-backup.sql drwxr-xr-x 3 www-data www-data 4096 Jan 17 00:54 html drwxrwx--- 10 www-data www-data 4096 Jan 17 06:55 moodledata ``` **IMPORTANT NOTE** It is important to realize that the `-o StrictHostKeyChecking=no` option in the above SSH command presents a security risk. It is included here to facilitate automated validation of these commands. It is not recommended to use this option in production environments, instead run the command manually and validate the host key. Subsequent executions of an SSH command will not require this validation step. For more information there is an excellent [superuser.com Q&A](https://superuser.com/questions/421074/ssh-the-authenticity-of-host-host-cant-be-established/421084#421084). ### If you set `htmlLocalCopySwitch` to true (this is the default now) Originally the `/moodle/html` directory was shared across all autoscaled web VMs through the specified file server (Gluster or NFS), and this is not good for web response time. Therefore, we introduced the `htmlLocalCopySwitch` that'll copy the `/moodle/html` directory to `/var/www/html` in each autoscaled web VM and reconfigures the web server (apache/nginx)'s server root directory accordingly, when it's set to true. This now requires directory sync between `/moodle/html` and `/var/www/html`, and currently it's addressed by simple polling (minutely). Therefore, if you are going to update your Moodle code/settings with the switch set to true, please follow the following steps: * Put your Moodle site to maintenance mode. * This will need to be done on the contoller VM with some shell command. * It should be followed by running the following command to propagate the change to all autoscaled web VMs: ```bash $ sudo /usr/local/bin/update_last_modified_time.moodle_on_azure.sh ``` * Once this command is executed, each autoscaled web VM will pick up (sync) the changes within 1 minute, so wait for one minute. * Then you can start updating your Moodle code/settings, like installing/updating plugins or upgrading Moodle version or changing Moodle configurations. Again, note that this should be all done on the controller VM using some shell commands. * When you are done updating your Moodle code/settings, run the same command as above to let each autoscaled web VM pick up (sync) the changes (wait for another minute here, for the same reason). Please do let us know on this Github repo's Issues if you encounter any problems with this process. ## Getting an SQL dump By default a daily sql dump of your database is taken at 02:22 and saved to `/moodle/db-backup.sql`(.gz). This file can be retrieved using SCP or similar. For example: ``` bash scp azureadmin@$MOODLE_CONTROLLER_INSTANCE_IP:/moodle/db-backup.sql /tmp/moodle-db-backup.sql ``` To obtain a more recent SQL dump you run the commands appropriate for your chosen database on the Controller VM. The following sections will help with this task. #### Postgres Postgress provides a `pg_dump` command that can be used to take a snapshot of the database via SSH. For example, use the following command: ``` bash ssh azureadmin@$MOODLE_CONTROLLER_INSTANCE_IP 'pg_dump -Fc -h $MOODLE_DATABASE_DNS -U $MOODLE_DATABASE_ADMIN_USERNAME moodle > /moodle/db-snapshot.sql' ``` See the Postgres documentation for full details of the [`pg_dump`](https://www.postgresql.org/docs/9.5/static/backup-dump.html) command. #### MySQL MySQL provides a `mysql_dump` command that can be used to take a snapshot of the database via SSH. For example, use the following command: ``` bash ssh azureadmin@$MOODLE_CONTROLLER_INSTANCE_IP 'mysqldump -h $mysqlIP -u ${azuremoodledbuser} -p'${moodledbpass}' --databases ${moodledbname} | gzip > /moodle/db-backup.sql.gz' ``` ## Backup and Recovery If you have set the `azureBackupSwitch` in the input parameters to `1` then Azure will provide VM backups of your Gluster node. This is recommended as it contains both your Moodle code and your sitedata. Restoring a backed up VM is outside the scope of this doc, but Azure's documentation on Recovery Services can be found here: https://docs.microsoft.com/en-us/azure/backup/backup-azure-vms-first-look-arm ## Resizing your Database Note: This process involves site downtime and should therefore only be carried out during a planned maintenance window. At the time of writing Azure does not support resizing MySQL or Postgres databases. You can, however, create a new database instance, with a different size, and change your config to point to that. To get a different size database you'll need to: 1. [Place your Moodle site into maintenance mode](https://docs.moodle.org/34/en/Maintenance_mode). You can do this either via the web interface or the command line on the controller VM. 2. Perform an SQL dump of your database. See above for more details. 3. Create a new Azure database of the size you want inside your existing resource group. 4. Using the details in your /moodle/html/moodle/config.php create a new user and database matching the details in config.php. Make sure to grant all rights on the db to the user. 5. On the controller instance, change the db setting in /moodle/html/moodle/config.php to point to the new database. 6. Take Moodle site out of maintenance mode. 7. Once confirmed working, delete the previous database instance. How long this takes depends entirely on the size of your database and the speed of your VM tier. It will always be a large enough window to make a noticeable outage. ## Changing the SSL cert The self-signed cert generated by the template is suitable for very basic testing, but a public website will want a real cert. After purchasing a trusted certificate, it can be copied to the following files to be ready immediately: - /moodle/certs/nginx.key: Your certificate's private key - /moodle/certs/nginx.crt: Your combined signed certificate and trust chain certificate(s). ## Managing Azure DDoS protection By default, every plublic IP is protected by Azure DDoS protection Basic SKU. You can find more information about Azure DDoS protection Basic SKU [here](https://docs.microsoft.com/en-us/azure/virtual-network/ddos-protection-overview). If you want more protection, you can activate Azure DDoS protection Standard SKU by setting the ddosSwith to true. You can find how to work with Azure DDoS protection plan [here](https://docs.microsoft.com/en-us/azure/virtual-network/manage-ddos-protection#work-with-ddos-protection-plans). If you want to disable the Azure DDoS protection, you can follow the instruction [here](https://docs.microsoft.com/en-us/azure/virtual-network/manage-ddos-protection#disable-ddos-for-a-virtual-network). Be careful, disabling the Azure DDoS protection on your vnet will not stop the fee. You have to delete the Azure DDoS protection plan if you want to stop the fee. If you have deployed your cluster without Azure DDoS protection plan, you still can activate the Azure DDoS protection plan thanks to the instruction [here](https://docs.microsoft.com/en-us/azure/virtual-network/manage-ddos-protection#enable-ddos-for-an-existing-virtual-network). ## Next Steps 1. [Retrieve configuration details using CLI](./Get-Install-Data.md) ================================================ FILE: docs/Parameters.md ================================================ # Moodle on Azure Parameters Our goal with these templates is to make it as easy as possible to deploy a Moodle on Azure cluster that can be customized to your specific needs. To that end we provide a great manay configuration options. This document attempts to document all these parameters, however, like all documentation it can sometimes fall behind. For a canonical reference you should review the `azuredeploy.json` file. ## Extracting documentation from azuredeploy.json To make it a litte easier to read `azuredeploy.json` you might want to run the following commands which will extract the necessary information and display it in a more readable form. ```bash sudo apt install jq ``` ``` bash jq -r '.parameters | to_entries[] | "### " + .key + "\n\n" + .value.metadata.description + "\n\nType: " + .value.type + "\n\nPossible Values: " + (.value.allowedValues | @text) + "\n\nDefault: " + (.value.defaultValue | @text) + "\n\n"' azuredeploy.json ``` ## Available Parameters ### _artifactsLocation The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated. Type: string Possible Values: null Default: https://raw.githubusercontent.com/Azure/Moodle/master/ ### _artifactsLocationSasToken The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated. Type: securestring Possible Values: null Default: ### applyScriptsSwitch Switch to process or bypass all scripts/extensions Type: bool Possible Values: null Default: true ### azureBackupSwitch Switch to configure AzureBackup and enlist VM's Type: bool Possible Values: null Default: false ### redisDeploySwitch Switch to deploy a redis cache or not. Note that certain versions of Moodle (e.g., 3.1) don't work well with Redis, so use this only for known well-working Moodle versions (e.g., 3.4). Type: bool Possible Values: null Default: false ### vnetGwDeploySwitch Switch to deploy a virtual network gateway or not Type: bool Possible Values: null Default: false ### installObjectFsSwitch Switch to install Moodle Object FS plugins (with Azure Blob storage) Type: bool Possible Values: null Default: false ### installO365pluginsSwitch Switch to install Moodle Office 365 plugins. As of May 22, 2018, O365 plugins for Moodle 3.5 haven't been released, so to set this true, you must set the moodleVersion to 3.4 or below. Type: bool Possible Values: null Default: false ### installGdprPluginsSwitch (Should be used only for Moodle 3.4 & 3.3) Switch to install Moodle GDPR plugins. Note these require Moodle versions 3.4.2+ or 3.3.5+ and these are included by default in Moodle 3.5. So if you choose MOODLE_35_STABLE as your moodleVersion, do not set this to true. Type: bool Possible Values: null Default: false ### htmlLocalCopySwitch Switch to create a local copy of /moodle/html or not Type: bool Possible Values: null Default: true ### ddosSwitch Switch to create a DDoS protection plan Type: bool Possible Values: null Default: false ### enableAccelNwForCtlrVmSwitch Switch to enable Azure Accelerated Networking on the controller VM. Default to false because currently the default controller VM SKU (D1) doesn't support AN. Change this to true if you set the controller VM SKU to eligibible ones (e.g., D2) for better performance. Type: bool Possible Values: null Default: false ### enableAccelNwForOtherVmsSwitch Switch to enable Azure Accelerated Networking on all other VMs. Default to true because currently the default controller VM SKU for all other VMS (D2) does support AN. Change this to false if you set the SKU of any other VMs to an ineligibible one (e.g., D1) to avoid deployment failure. Type: bool Possible Values: null Default: true ### httpsTermination Indicates where https termination occurs. 'VMSS' is for https termination at the VMSS instance VMs (using nginx https proxy). 'AppGw' is for https termination with an Azure Application Gateway. When selecting this, you need to specify all appGw* parameters. 'None' is for testing only with no https. 'None' may not be used with a separately configured https termination layer. If you want to use the 'None' option with your separately configured https termination layer, you'll need to update your Moodle config.php manually for $cfg->wwwroot and $cfg->sslproxy. Type: string Possible Values: ["VMSS","AppGw","None"] Default: VMSS ### siteURL URL for Moodle site Type: string Possible Values: null Default: www.example.org ### moodleVersion The Moodle version you want to install. Type: string Possible Values: ["MOODLE_35_STABLE","MOODLE_34_STABLE","v3.4.3","v3.4.2","v3.4.1","MOODLE_33_STABLE","MOODLE_32_STABLE","MOODLE_31_STABLE","MOODLE_30_STABLE","MOODLE_29_STABLE"] Default: MOODLE_35_STABLE ### sshPublicKey ssh public key Type: string Possible Values: null Default: null ### sshUsername ssh user name Type: string Possible Values: null Default: azureadmin ### controllerVmSku VM size for the controller VM Type: string Possible Values: null Default: Standard_DS1_v2 ### webServerType Web server type Type: string Possible Values: ["apache","nginx"] Default: apache ### autoscaleVmSku VM size for autoscaled web VMs Type: string Possible Values: null Default: Standard_DS2_v2 ### autoscaleVmCountMax Maximum number of autoscaled web VMs Type: int Possible Values: null Default: 10 ### autoscaleVmCountMin Minimum (also initial) number of autoscaled web VMs Type: int Possible Values: null Default: 1 ### osDiskStorageType Azure storage type for all VMs' OS disks. With htmlLocalCopySwith true, Premium_LRS (SSD) is strongly recommended, as PHP files will be served from OS disks. Type: string Possible Values: ["Premium_LRS","Standard_LRS"] Default: Premium_LRS ### dbServerType Database type Type: string Possible Values: ["postgres","mysql","mssql"] Default: mysql ### dbLogin Database admin username Type: string Possible Values: null Default: dbadmin ### mysqlPgresVcores MySql/Postgresql vCores. For Basic tier, only 1 & 2 are allowed. For GeneralPurpose tier, 2, 4, 8, 16, 32 are allowed. For MemoryOptimized, 2, 4, 8, 16 are allowed. Type: int Possible Values: [1,2,4,8,16,32] Default: 2 ### mysqlPgresStgSizeGB MySql/Postgresql storage size in GB. Minimum 5GB, increase by 1GB, up to 1TB (1024 GB) Type: int Possible Values: null Default: 125 ### mysqlPgresSkuTier MySql/Postgresql sku tier Type: string Possible Values: ["Basic","GeneralPurpose","MemoryOptimized"] Default: GeneralPurpose ### mysqlPgresSkuHwFamily MySql/Postgresql sku hardware family. Central US is Gen4 only, so make sure to change this parameter to Gen4 if your deployment is on Central US. Type: string Possible Values: ["Gen4","Gen5"] Default: Gen5 ### mysqlVersion Mysql version Type: string Possible Values: ["5.6","5.7"] Default: 5.7 ### postgresVersion Postgresql version Type: string Possible Values: ["9.5","9.6"] Default: 9.6 ### sslEnforcement MySql/Postgresql SSL connection Type: string Possible Values: ["Disabled","Enabled"] Default: Disabled ### mssqlDbServiceObjectiveName MS SQL database service object names Type: string Possible Values: ["S1","S2","S3","S4","S5","S6","S7","S9"] Default: S1 ### mssqlDbSize MS SQL database size Type: string Possible Values: ["100MB","250MB","500MB","1GB","2GB","5GB","10GB","20GB","30GB","40GB","50GB","100GB","250GB","300GB","400GB","500GB","750GB","1024GB"] Default: 250GB ### mssqlDbEdition MS SQL DB edition Type: string Possible Values: ["Basic","Standard"] Default: Standard ### mssqlVersion Mssql version Type: string Possible Values: ["12.0"] Default: 12.0 ### fileServerType File server type: GlusterFS, NFS, and NFS-HA (2-VM highly available NFS cluster) Type: string Possible Values: ["gluster","nfs","nfs-ha","nfs-byo"] Default: nfs ### nfsByoIpExportPath IP address and export path of the BYO-NFS share when fileServerType == nfs-byo. E.g., 172.16.1.8:/msazure Type: string Possible Values: null Default: ### fileServerDiskSize Size per disk for gluster nodes or nfs server Type: int Possible Values: null Default: 127 ### fileServerDiskCount Number of disks in raid0 per gluster node or nfs server Type: int Possible Values: null Default: 4 ### fileServerVmSku VM size for the gluster or NFS-HA nodes Type: string Possible Values: null Default: Standard_DS2_v2 ### keyVaultResourceId (VMSS https termination only) Azure Resource Manager resource ID of the Key Vault in case you stored your SSL cert in an Azure Key Vault (Note that this Key Vault must have been pre-created on the same Azure region where this template is being deployed). Leave this blank if you didn't. Resource ID example: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/xxx/providers/Microsoft.KeyVault/vaults/yyy. This value can be obtained from keyvault.sh output if you used the script to store your SSL cert in your Key Vault. Type: string Possible Values: null Default: ### sslCertKeyVaultURL (VMSS https termination only) Azure Key Vault URL for your stored SSL cert. This value can be obtained from keyvault.sh output if you used the script to store your SSL cert in your Key Vault. This parameter is ignored if the keyVaultResourceId parameter is blank. Type: string Possible Values: null Default: ### sslCertThumbprint (VMSS https termination only) Thumbprint of your stored SSL cert. This value can be obtained from keyvault.sh output if you used the script to store your SSL cert in your Key Vault. This parameter is ignored if the keyVaultResourceId parameter is blank. Type: string Possible Values: null Default: ### caCertKeyVaultURL (VMSS https termination only) Azure Key Vault URL for your stored CA (Certificate Authority) cert. This value can be obtained from keyvault.sh output if you used the script to store your CA cert in your Key Vault. This parameter is ignored if the keyVaultResourceId parameter is blank. Type: string Possible Values: null Default: ### caCertThumbprint (VMSS https termination only) Thumbprint of your stored CA cert. This value can be obtained from keyvault.sh output if you used the script to store your CA cert in your Key Vault. This parameter is ignored if the keyVaultResourceId parameter is blank. Type: string Possible Values: null Default: ### appGwSslCertKeyVaultResourceId (App Gateway https termination only) Azure Key Vault URL for your stored SSL cert, again for App Gateway https termination case only. (Note that this Key Vault must have been pre-created on the same Azure region where this template is being deployed). Leave this blank if you didn't. Resource ID example: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/xxx/providers/Microsoft.KeyVault/vaults/yyy. Type: string Possible Values: null Default: ### appGwSslCertKeyVaultSecretName (App Gateway https termination only) Name of the Azure Key Vault secret that's stored in the previously specified Key Vault as a PFX certificate (with no password) for your site's SSL cert. This secret must be pre-populated in the specified Key Vault with the matching name. Type: string Possible Values: null Default: ### appGwSkuName (App Gateway https termination only) Name of the Applicate Gateway SKU Type: string Possible Values: ["Standard_Small","Standard_Medium","Standard_Large","WAF_Medium","WAF_Large"] Default: Standard_Medium ### appGwSkuTier (App Gateway https termination only) Tier of the Applicate Gateway Type: string Possible Values: ["Standard","WAF"] Default: Standard ### appGwSkuCapacity (App Gateway https termination only) Capacity instance count) of the Applicate Gateway Type: int Possible Values: null Default: 2 ### storageAccountType Storage Account type. This storage account is only for the Moodle ObjectFS plugin and/or the (currently disabled) Azure Files file share option Type: string Possible Values: ["Standard_LRS","Standard_GRS","Standard_ZRS"] Default: Standard_LRS ### searchType options of moodle global search Type: string Possible Values: ["none","azure","elastic"] Default: none ### tikaService options of enabling tika service for file searching in moodle Type: string Possible Values: ["none","tika"] Default: none ### azureSearchSku the search service level you want to create. Type: string Possible Values: ["free","basic","standard","standard2","standard3"] Default: basic ### azureSearchReplicaCount Replicas distribute search workloads across the service. You need 2 or more to support high availability (applies to Basic and Standard only). Type: int Possible Values: null Default: 3 ### azureSearchPartitionCount Partitions allow for scaling of document count as well as faster indexing by sharding your index over multiple Azure Search units. Type: int Possible Values: [1,2,3,4,6,12] Default: 1 ### azureSearchHostingMode Applicable only for azureSearchSku set to standard3. You can set this property to enable a single, high density partition that allows up to 1000 indexes, which is much higher than the maximum indexes allowed for any other azureSearchSku. Type: string Possible Values: ["default","highDensity"] Default: default ### elasticVmSku VM size for the elastic search nodes Type: string Possible Values: null Default: Standard_DS2_v2 ### tikaVmSku VM size for the tika search nodes Type: string Possible Values: null Default: Standard_DS2_v2 ### customVnetId Azure Resource ID of the Azure virtual network where you want to deploy your Moodle resources. A vnet resource ID is of the following format: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx/resourceGroups/gggg/providers/Microsoft.Network/virtualNetworks/vvvv. Note that this virtual network must be on the same Azure location as this template deployment location. If this parameter is blank, a new Azure virtual network will be created and used. In that case, the address space of the newly created virtual network will be */16 of the following vNetAddressSpace parameter value below. Type: string Possible Values: null Default: ### vNetAddressSpace Address range for the Moodle virtual network and various subnets - presumed /16 for a newly created vnet in case customVnetId is blank. Further subneting (a number of */24 subnets starting from the xxx.yyy.zzz.0/24 will be created on a newly created vnet or your BYO-vnet (specified in customVnetId parameter). Type: string Possible Values: null Default: 172.31.0.0 ### gatewayType Virtual network gateway type Type: string Possible Values: ["Vpn","ER"] Default: Vpn ### vpnType Virtual network gateway vpn type Type: string Possible Values: ["RouteBased","PolicyBased"] Default: RouteBased ### loadBalancerSku Loadbalancer SKU Type: string Possible Values: ["Basic","Standard"] Default: Basic ### location Azure Location for all resources. Type: string Possible Values: null Default: [resourceGroup().location] ================================================ FILE: docs/Preparation.md ================================================ # Environment Preparation This document describes how to ensure your environment is configured for working with Moodle on Azure. ## Prerequisites In order to configure our deployment and tools we'll set up some [environment variables](./Environment-Variables.md) to ensure consistency. ## Required software We'll use a number of tools when working with Moodle on Azure. Let's ensure they are all installed: ``` shell sudo apt-get update sudo apt-get install wget -y sudo apt-get openssh-client -y ``` The [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest) is also important: ```bash AZ_REPO=$(lsb_release -cs) echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | sudo tee /etc/apt/sources.list.d/azure-cli.list sudo apt-key adv --keyserver packages.microsoft.com --recv-keys 52E16F86FEE04B979B07E28DB02C46DF417A0893 sudo apt-get install apt-transport-https sudo apt-get update && sudo apt-get install azure-cli ``` ## Ensure we have a valid SSH key pair We use SSH for secure communication with our hosts. The following line will check there is a valid SSH key available and, if not, create one. ``` if [ ! -f "$MOODLE_SSH_KEY_FILENAME" ]; then ssh-keygen -t rsa -N "" -f $MOODLE_SSH_KEY_FILENAME; fi ``` ## Checkout the Moodle ARM Template The Moodle Azure Resource Manager template is hosted on GitHub. We'll checkout the template into our workspace. ``` git clone git@github.com:Azure/Moodle.git $MOODLE_AZURE_WORKSPACE/arm_template ``` # Validation After completing these steps we should have, amonst other things, a complete checkout of the Moodle templates from GitHub: ``` bash ls $MOODLE_AZURE_WORKSPACE/arm_template ``` Results: ``` expected_similarity=0.4 azuredeploy.json azuredeploy.parameters.json CONTRIBUTE.md docs env.json etc images LICENSE LICENSE-DOCS metadata.json nested README.md ``` We should also have a number of applications installed, such as the Azure CLI: ``` bash if hash az 2>/dev/null; then echo "Azure CLI Installed"; else echo "Missing dependency: Azure CLI"; fi ``` ``` AzureCLI Installed ``` ================================================ FILE: docs/SslCert.md ================================================ # SSL Certificate Management A valid SSL (TLS) certificate should be used with your domain name for the Moodle site to be deployed using the templates. By default, the templates will configure the HTTPS server with a self-signed SSL server certificate/private key, which can be manually changed with your own valid SSL server certificate/private key after the deployment. If you'd like to configure the Moodle cluster (to be deployed) with your own domain and your valid SSL server certificate/private key, then you can do so by utilizing [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) and configuring the related template parameters as described below. This support is based on [another similar work](https://github.com/Azure/azure-quickstart-templates/tree/master/201-vmss-ubuntu-web-ssl) and adapted to our situation. ## Initial deployment To configure the Moodle cluster (to be deployed) with your purchased SSL certificate, currently the related files should be stored in an Azure Key Vault as secrets, so that Azure Resource Manager can reference when it deploys VMs as specified in templates. You can create your own Azure Key Vault and store your purchased SSL certificate (called 'import' in Azure Key Vault terminology) by following related documentation like [this](https://docs.microsoft.com/en-us/azure/key-vault/key-vault-manage-with-cli2). However, the related files must be stored in a specific format, so we created a shell script (`keyvault.sh`) that will perform all necessary steps for this purpose. To use this script, you'll first need to upload your SSL certificate/private key files (.pem) to your deployment environment you set up by following the [preparation document](Preparation.md) (a Linux command line). The .pem files should be as follows: - `cert.pem`: The SSL certificate only in PEM format - `key.pem`: The private key for the SSL certificate only in PEM format - `chain.pem`: This is optional in case your server certificate is signed by an intermediate CA (Certificate Authority) certificate, instead of a root CA certificate. Currently only one intermediate CA certificate is supported by the script. Once you updloaded the files to your deployment environment, you can run the following command to create an Azure Key Vault on your subscription and store your SSL certificate, private key, and optionally the intermediate CA certificate: ``` bash $ bash $MOODLE_AZURE_WORKSPACE/arm_template/etc/keyvault.sh cert.pem key.pem chain.pem ``` Make sure to set `` the same as the Azure region you'll be using to deploy the Moodle template. Assign desired names for ``, `` (you can use an existing resource group) and ``. `` is not very important in our deployment. Then you'll get outputs as follows: ``` ... Specified SSL cert/key .pem files are now stored in your Azure Key Vault and ready to be used by the template. Use the following values for the related template parameters: - keyVaultResourceId: /subscriptions/206c66fc-a48c-480d-ad06-0d429e82c586/resourceGroups/keyvault/providers/Microsoft.KeyVault/vaults/mdl-kv - sslCertKeyVaultURL: https://mdl-kv.vault.azure.net/secrets/mymoodlesitecert/4c88452fe72b4d469253af48348f4944 - sslCertThumbprint: 56478E4F9555662476E2763D909F50B3DD26FF84 - caCertKeyVaultURL: https://mdl-kv.vault.azure.net/secrets/camymoodlesitecert/684efab1f2124e71a2c809457d10808b - caCertThumbprint: E6A3B45B062D509B3382282D196EFE97D5956CCB Done ``` This example outputs assumes `"keyvault"` is used for ``, `"mdl-kv"` for ``, and `"mymoodlesitecert"` for ``. Note that `caCertKeyVaultURL` and `caCertThumbprint` will be empty if you didn't specify `chain.pem`. Then you can copy these outputs to the template's corresponding parameters, and Azure Resource Manager will install the certificate and the private key on the deployed VMs and the deployed HTTPS server will use this certificate and private key. ## Certificate rotation Another important benefit of using Azure Key Vault is to handle certificate expiration/rotation automatically. Unfortunately, the current implementation doesn't support the auto-rotation. So when it becomes near your SSL certificate's expiry, you'll need to manually update the deployed certificate and private key files (it's in `/moodle/certs/nginx.{crt,key}` on the controller VM) and restart all the web frontend VM instances. We'll improve our implementation to support auto-rotation in the future. ================================================ FILE: docs/Test.md ================================================ # Test a Moodle Instance ## Prerequisites It is obviously necessary to have a [Moodle cluster up and running](./Deploy.md). ## Next Steps * [Delete all Resources](./Delete.md) ================================================ FILE: docs/env.json ================================================ { "MOODLE_RG_NAME": "rgmoodlearm13tagging", "MOODLE_RG_LOCATION": "canadacentral", "MOODLE_DEPLOYMENT_NAME": "MainDeployment" } ================================================ FILE: env.json ================================================ { "MOODLE_RG_NAME": "rgmoodlearm12", "MOODLE_RG_LOCATION": "canadacentral", "MOODLE_DEPLOYMENT_NAME": "MainDeployment" } ================================================ FILE: etc/changeBranchInURL.sh ================================================ #!/bin/bash from=$1 to=$2 sed -i s/%2F${from}%2F/%2F${to}%2F/g README.md sed -i s#/${from}/#/${to}/#g azuredeploy.json ================================================ FILE: etc/checkBaseUrls.sh ================================================ # Ensure that the Base URL for templates, scripts and deploy to Azure buttons # is correctly set for the current git branch. # Correct values for locations CURRENT_BRANCH=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p') echo "Current git branch is '$CURRENT_BRANCH'" BASE_TEMPLATE_URL=https://raw.githubusercontent.com/Azure/Moodle/$CURRENT_BRANCH/nested/ echo "Base template URL: $BASE_TEMPLATE_URL" SCRIPT_LOCATION=https://raw.githubusercontent.com/Azure/Moodle/$CURRENT_BRANCH/scripts/ echo "Script location: $SCRIPT_LOCATION" DEPLOY_TO_AZURE_URL=https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FMoodle%2F$CURRENT_BRANCH%2Fazuredeploy.json echo "Deploy to Azure URL: $DEPLOY_TO_AZURE_URL" VISUALIZE_URL=http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FMoodle%2F$CURRENT_BRANCH%2Fazuredeploy.json echo "Visualize template URL: $VISUALIZE_URL" # Check values in README.md VALUE=$(sed -n -e 's/.*deploybutton.png)](\([^)]*\)).*/\1/p' README.md) if [[ "$VALUE" = "$DEPLOY_TO_AZURE_URL" ]] then echo "Deploy to Azure URL is set correctly" else echo "!!!!! Deploy to Azure URL is not set correctly in README.md, it is currently:" echo $VALUE fi VALUE=$(sed -n -e 's/.*visualizebutton.png)](\([^)]*\)).*/\1/p' README.md) if [[ "$VALUE" = "$VISUALIZE_URL" ]] then echo "Visualize URL is set correctly" else echo "!!!!! Visualize URL is not set correctly in README.md, it is currently:" echo $VALUE fi # Check values in azuredeploy.json VALUE=$(sed -n -e 's/.*\"baseTemplateUrl\": \"\([^\"]*\)\",/\1/p' azuredeploy.json) if [[ "$VALUE" = "$BASE_TEMPLATE_URL" ]] then echo "baseTemplateURL is set correctly" else echo "!!!!! baseTemplateURL is not set correctly, it is currently:" echo $VALUE fi VALUE=$(sed -n -e 's/.*\"scriptLocation\": \"\([^\"]*\)\",/\1/p' azuredeploy.json) if [[ "$VALUE" = "$SCRIPT_LOCATION" ]] then echo "scriptLocation is set correctly" else echo "!!!!! scriptLocation is not set correctly, it is currently:" echo $VALUE fi ================================================ FILE: etc/keyvault.sh ================================================ #!/bin/bash # Based on https://github.com/Azure/azure-quickstart-templates/blob/master/201-vmss-ubuntu-web-ssl/keyvault.sh #set -e usage() { echo usage: keyvault.sh ' ' echo The cacertpem file is optional. The template will accept a self-signed cert and key. } creategroup() { local group=$(az group show -g $rgname) if [ -n "$group" ]; then echo Resource Group $rgname already exists. Skipping creation. else # Create a resource group for the keyvault az group create -n $rgname -l $location fi } createkeyvault() { az keyvault show -n $vaultname 2> /dev/null if [ $? -eq 0 ] then echo Key Vault $vaultname already exists. Skipping creation. else echo Creating Key Vault $vaultname. creategroup # Create the key vault az keyvault create --name $vaultname --resource-group $rgname --location $location --enabled-for-template-deployment true --enabled-for-deployment true fi } convertcert() { local cert=$1 local key=$2 local pfxfile=$3 local pass=$4 echo Creating PFX $pfxfile openssl pkcs12 -export -out $pfxfile -inkey $key -in $cert -password pass:$pass 2> /dev/null if [ $? -eq 1 ] then echo problem converting $key and $cert to pfx exit 1 fi fingerprint=$(openssl x509 -in $cert -noout -fingerprint | cut -d= -f2 | sed 's/://g' ) } convertcacert() { local cert=$1 local pfxfile=$2 local pass=$3 echo Creating PFX $pfxfile openssl pkcs12 -export -out $pfxfile -nokeys -in $cert -password pass:$pass 2> /dev/null if [ $? -eq 1 ] then echo problem converting $cert to pfx exit 1 fi fingerprint=$(openssl x509 -in $cert -noout -fingerprint | cut -d= -f2 | sed 's/://g' ) } storesecret() { local secretfile=$1 local name=$2 filecontentencoded=$( cat $secretfile | base64 $base64_unwrap ) json=$(cat << EOF { "data": "${filecontentencoded}", "dataType" :"pfx", "password": "${pwd}" } EOF ) jsonEncoded=$( echo $json | base64 $base64_unwrap ) r=$(az keyvault secret set --vault-name $vaultname --name $name --value $jsonEncoded) if [ $? -eq 1 ] then echo problem storing secret $name in $vaultname exit 1 fi id=$(az keyvault secret show --vault-name $vaultname --name $name --query id -o tsv) echo Secret ID is $id } # We need at least 6 parameters if [ "$#" -lt 6 ]; then usage exit fi # The base64 command on OSX does not know about the -w parameter, but outputs unwrapped base64 by default base64_unwrap="-w 0" [[ $(uname) == "Darwin" ]] && base64_unwrap="" vaultname=$1 rgname=$2 location=$3 secretname=$4 certfile=$5 keyfile=$6 cacertfile=$7 # Create a random password with 33 bytes of entropy # I picked 33 so the last character will not be = pwd=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64) certpfxfile=${certfile%.*crt}.pfx cacertpfxfile=${cacertfile%.*crt}.pfx casecretname=ca$secretname createkeyvault # converting SSL cert to pfx convertcert $certfile $keyfile $certpfxfile $pwd certprint=$fingerprint echo $certpfxfile fingerprint is $fingerprint # storing pfx in keyvault echo Storing $certpfxfile as $secretname storesecret $certpfxfile $secretname certid=$id rm -f $certpfxfile if [ ! -z $cacertfile ] then # converting CA cert to pfx convertcacert $cacertfile $cacertpfxfile $pwd echo $cacertpfxfile fingerprint is $fingerprint cacertprint=$fingerprint # storing pfx in key vault echo Storing $cacertpfxfile as $casecretname storesecret $cacertpfxfile $casecretname cacertid=$id rm -f $cacertpfxfile fi echo "Specified SSL cert/key .pem files are now stored in your Azure Key Vault and ready to be used by the template." echo "Use the following values for the related template parameters:" echo echo "- keyVaultResourceId: $(az keyvault show --name $vaultname --query id -o tsv)" echo "- sslCertKeyVaultURL: $certid" echo "- sslCertThumbprint: $certprint" echo "- caCertKeyVaultURL: $cacertid" echo "- caCertThumbprint: $cacertprint" echo Done ================================================ FILE: etc/travis/Configuration.py ================================================ import json import os import time from azure.mgmt.resource.resources.v2017_05_10.models import DeploymentMode class Configuration: def __init__(self): self.deployment_name = 'azuredeploy' self.client_id = os.getenv('SPNAME') self.secret = os.getenv('SPPASSWORD') self.tenant_id = os.getenv('SPTENANT') self.location = os.getenv('LOCATION', 'westus2') self.source_branch = self.identify_source_branch() self.fullci_branches = os.getenv('FULLCI_BRANCHES', 'master').split(':') self.commit_message = os.getenv('TRAVIS_COMMIT_MESSAGE', None) self.ssh_key = self.identify_ssh_key() self.resource_group = self.identify_resource_group() self.deployment_properties = self.generate_deployment_properties() def identify_resource_group(self): resource_group = os.getenv('RESOURCEGROUP') if resource_group is None: resource_group = 'azmdl-travis-' + os.getenv('TRAVIS_BUILD_NUMBER', 'manual-{}'.format(time.time())) return resource_group def identify_ssh_key(self): ssh_key = os.getenv('SPSSHKEY') if ssh_key is None: with open('azure_moodle_id_rsa.pub', 'r') as sshkey_fd: ssh_key = sshkey_fd.read() return ssh_key def generate_deployment_properties(self): with open('azuredeploy.json', 'r') as template_fd: template = json.load(template_fd) with open('azuredeploy.parameters.json', 'r') as parameters_fd: parameters = json.load(parameters_fd) parameters = parameters['parameters'] parameters['sshPublicKey']['value'] = self.ssh_key parameters['_artifactsLocation'] = {'value': self.identify_artifacts_location()} return { 'mode': DeploymentMode.incremental, 'template': template, 'parameters': parameters, } def identify_artifacts_location(self): slug = os.getenv('TRAVIS_PULL_REQUEST_SLUG') if not slug: slug = os.getenv('TRAVIS_REPO_SLUG') return "https://raw.githubusercontent.com/{}/{}/".format(slug, self.source_branch) def identify_source_branch(self): branch = os.getenv('TRAVIS_PULL_REQUEST_BRANCH') if not branch: branch = os.getenv('TRAVIS_BRANCH') return branch def is_valid(self): valid = True for key, value in vars(self).items(): if value is None: valid = False print('(missing configuration for {})'.format(key)) if self.deployment_properties['parameters']['_artifactsLocation']['value'] is None: valid = False print('(could not identify _artifactsLocation)') return valid def should_run_full_ci(self): if self.source_branch in self.fullci_branches: return True message = self.commit_message.upper() if '[FULL CI]' in message or '[FULLCI]' in message: return True return False ================================================ FILE: etc/travis/DeploymentTester.py ================================================ import os import pycurl import sys import tempfile import time import urllib from io import BytesIO from pycurl import Curl from azure.mgmt.resource import ResourceManagementClient from azure.mgmt.subscription import SubscriptionClient from msrestazure.azure_active_directory import ServicePrincipalCredentials from travis.Configuration import Configuration class DeploymentTester: @staticmethod def elapsed(since): elapsed = int(time.time() - since) elapsed = '{:02d}:{:02d}:{:02d}'.format(elapsed // 3600, (elapsed % 3600 // 60), elapsed % 60) return elapsed def __init__(self): self.config = Configuration() self.deployment = None self.credentials = None """:type : ServicePrincipalCredentials""" self.resource_client = None """:type : ResourceManagementClient""" def run(self): should_delete_resource_group = False try: self.check_configuration() self.login() self.create_resource_group() self.validate() if not self.config.should_run_full_ci(): print('\n\nBasic CI tests successful.') should_delete_resource_group = True return self.deploy() self.moodle_smoke_test() self.moodle_admin_login() print('\n\nFull CI tests successful!') should_delete_resource_group = True finally: if should_delete_resource_group: self.delete_resource_group() def check_configuration(self): print('\nChecking configuration...') if not self.config.is_valid(): print('No Azure deployment info given, skipping test deployment and exiting.') print('Further information: https://github.com/Azure/Moodle#automated-testing-travis-ci') sys.exit() artifacts_location = self.config.deployment_properties['parameters']['_artifactsLocation'] print('- Detected "_artifactsLocation": ' + artifacts_location['value']) print("(all check)") def login(self): print('\nLogging in...') self.credentials = ServicePrincipalCredentials( client_id=self.config.client_id, secret=self.config.secret, tenant=self.config.tenant_id, ) print('(got credentials)') subscription_client = SubscriptionClient(self.credentials) subscription = next(subscription_client.subscriptions.list()) print('(found subscription)') self.resource_client = ResourceManagementClient(self.credentials, subscription.subscription_id) print("(logged in)") def create_resource_group(self): print('\nCreating group "{}" on "{}"...'.format(self.config.resource_group, self.config.location)) self.resource_client.resource_groups.create_or_update(self.config.resource_group, {'location': self.config.location}) print('(created)') def validate(self): print('\nValidating template...') validation = self.resource_client.deployments.validate(self.config.resource_group, self.config.deployment_name, self.config.deployment_properties) if validation.error is not None: print("*** VALIDATION FAILED ({}) ***".format(validation.error)) print(validation.error.message) for detail in validation.error.details: print("- {}:\n{}".format(detail.code, detail.message)) sys.exit(1) print("(valid)") def deploy(self): print('\nDeploying template, feel free to take a nap...') deployment = self.resource_client.deployments.create_or_update(self.config.resource_group, self.config.deployment_name, self.config.deployment_properties) """:type : msrestazure.azure_operation.AzureOperationPoller""" started = time.time() while not deployment.done(): print('... after {} still "{}" ...'.format(self.elapsed(started), deployment.status())) deployment.wait(60) print("WAKE UP! After {} we finally got status {}.".format(self.elapsed(started), deployment.status())) print("Checking deployment response...") properties = deployment.result(0).properties if properties.provisioning_state != 'Succeeded': print("*** DEPLOY FAILED ***") print('Provisioning state: ' + properties.provisioning_state) sys.exit(1) self.load_deployment_outputs(properties.outputs) print("(success)") def load_deployment_outputs(self, outputs): self.deployment = {} for key, value in outputs.items(): self.deployment[key] = value['value'] print("- Found: " + key) def moodle_smoke_test(self): print("\nMoodle Smoke Test...") url = 'https://' + self.deployment['siteURL'] curl = Curl() curl.setopt(pycurl.URL, url) curl.setopt(pycurl.SSL_VERIFYPEER, False) curl.setopt(pycurl.WRITEFUNCTION, lambda x: None) curl.perform() status = curl.getinfo(pycurl.HTTP_CODE) if status != 200: print("*** DEPLOY FAILED ***") print('HTTP Status Code: {}'.format(status)) sys.exit(1) print('(ok: {})'.format(status)) def moodle_admin_login(self): print("\nLogging in into Moodle as 'admin'...") response = self.moodle_admin_login_curl() if 'Admin User' not in response: print("*** FAILED: 'Admin User' keyword not found ***") sys.exit(1) print('(it worked)') def moodle_admin_login_curl(self): fd, path = tempfile.mkstemp() try: response = BytesIO() url = 'https://' + self.deployment['siteURL'] + '/login/index.php' curl = Curl() curl.setopt(pycurl.URL, url) curl.setopt(pycurl.SSL_VERIFYPEER, False) curl.setopt(pycurl.WRITEFUNCTION, response.write) curl.setopt(pycurl.POST, True) curl.setopt(pycurl.COOKIEJAR, path) curl.setopt(pycurl.COOKIEFILE, path) post = urllib.parse.urlencode({'username': 'admin', 'password': self.deployment['moodleAdminPassword']}) curl.setopt(pycurl.POSTFIELDS, post) curl.setopt(pycurl.FOLLOWLOCATION, True) curl.perform() status = curl.getinfo(pycurl.HTTP_CODE) if status != 200: print("*** FAILED: {} ***".format(status)) sys.exit(1) response = response.getvalue().decode('utf-8') finally: os.remove(path) return response def delete_resource_group(self): print('\n\nDeleting the resource group for this passing build...') self.resource_client.resource_groups.delete(self.config.resource_group, polling=False) print('(delete initiated, not polling)') ================================================ FILE: etc/travis/__init__.py ================================================ ================================================ FILE: etc/travis.py ================================================ #!/usr/bin/env python3 from travis.DeploymentTester import DeploymentTester DeploymentTester().run() ================================================ FILE: etc/updateDocsParametersMd.sh ================================================ #!/bin/bash # Should be run at the git repo root as: $ ./etc/updateDocsParameters.sh dpkg -l jq &> /dev/null || sudo apt install jq sed -i '/## Available Parameters/q' docs/Parameters.md echo >> docs/Parameters.md jq -r '.parameters | to_entries[] | "### " + .key + "\n\n" + .value.metadata.description + "\n\nType: " + .value.type + "\n\nPossible Values: " + (.value.allowedValues | @text) + "\n\nDefault: " + (.value.defaultValue | @text) + "\n\n"' azuredeploy.json >> docs/Parameters.md ================================================ FILE: loadtest/Azure_Login.md ================================================ # Login to Azure Before we start a load test session we need to first veriy that we are logged in to Azure using the CLI. ## Azure Login ``` bash az login --username $AZURE_USERNAME --password $AZURE_PASWORD ``` Note that if your username or password has any special characters in it, such as '$' this may fail. You can login using a browser using `az login`. ``` bash az account set --subscription $AZURE_SUBSCRIPTION_ID ``` ## Validation ``` bash az account show ``` Results: ``` { "environmentName": "AzureCloud", "id": "325e7c34-99fb-4190-aa87-1df746c67705", "isDefault": true, "name": "Ross Dev Account", "state": "Enabled", "tenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47", "user": { "name": "rogardle@microsoft.com", "type": "user" } } ``` ================================================ FILE: loadtest/Deploy_Load_Test_VM.md ================================================ # Setting up a test host To run load tests using the resources in this directory, you'll want to spin up an Ubuntu VM (let's call it the jMeter host) in your Azure subscription. This should be located in the same region as your Moodle cluster in order to avoid egress charges. Once your VM is ready, you need to install Java and [jMeter](https://jmeter.apache.org/). ## Prerequisites To make things consistent across different sessions load testing Moodle we should [configure the moodle environment](../docs/Preparation.md). We will want to use a consistent resource group name in order to avoid wasting resource in these tests: ``` MOODLE_RG_NAME=loadtest ``` And we'll need a name for our load test VM: ``` MOODLE_LOAD_TEST_VM_NAME=LoadTestVM ``` ## Deploy the Load Test VM First you need a resource group within which all your resources will be deployed. ``` bash az group create --name $MOODLE_RG_NAME --location $MOODLE_RG_LOCATION ``` Now we can create our VM in this group. The following command will create the VM and, if necessary, generate the SSH keys. ``` bash az vm create --resource-group $MOODLE_RG_NAME --name $MOODLE_LOAD_TEST_VM_NAME --image UbuntuLTS --generate-ssh-keys ``` Results: ``` json { "fqdns": "", "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/loadtestvm/providers/Microsoft.Compute/virtualMachines/LoadTestVM", "location": "southcentralus", "macAddress": "00-0D-3A-70-91-57", "powerState": "VM running", "privateIpAddress": "10.0.0.4", "publicIpAddress": "13.84.131.173", "resourceGroup": "loadtestvm", "zones": "" } ``` You will need the IP number from this output. For convenience we'll place it into an environment variable: ``` bash ipAddress=$(az network public-ip show --name ${MOODLE_LOAD_TEST_VM_NAME}PublicIP --resource-group $MOODLE_RG_NAME --query "ipAddress" --output tsv) echo $ipAddress ``` We can now connect to the VM using ssh, and run commands. The first thing we want to do is pull down the Moodle on Azure repo. Since this document is used to automatically run tests all our commands need to be non-interactive. We will therefore skip the host key validation step. Note that you should never do this in a production environment (remove `-o StrictHostKeyChecking=no`): ``` bash ssh -o StrictHostKeyChecking=no $ipAddress "rm -Rf Moodle; git clone git://github.com/Azure/Moodle.git" ``` Now we can install the load testing scripts, we will have these loaded via the `.profile` so that they are always availble. ``` bash ssh $ipAddress 'echo ". ~/Moodle/loadtest/loadtest.sh" >> ~/.profile' ``` This script provides some helper functions for installing dependencies on the VM. ``` bash ssh $ipAddress 'install_java_and_jmeter; install_az_cli' ``` We need to login to Azure using the CLI. The command below is convenient but is not secure since it stores your password in clear text in an environment variable. However, it is convenient for test purposes. ``` bash ssh $ipAddress "az login --username $AZURE_LOGIN --password $AZURE_PASSWORD; az account set --subscription $AZURE_SUBSCRIPTION_ID" ``` ## Validation Finally, we will verify that key dependencies have been installed. First lets check Java is present: ``` bash ssh -o StrictHostKeyChecking=no $ipAddress "java -version" ``` Results: ``` openjdk version "1.8.0_151" OpenJDK Runtime Environment (build 1.8.0_151-8u151-b12-0ubuntu0.16.04.2-b12) OpenJDK 64-Bit Server VM (build 25.151-b12, mixed mode) ``` We will also need to confirm the Azure CLI is present: ``` bash ssh -o StrictHostKeyChecking=no $ipAddress "if hash az 2>/dev/null; then echo "Azure CLI Installed"; else echo "Missing dependency: Azure CLI"; fi" ``` Results: ``` Azure CLI Installed ``` ================================================ FILE: loadtest/README.md ================================================ # Load-Testing Deployed Moodle Cluster This directory currently contains utility scripts, a Moodle test course, and an Apache jMeter test plan that can be used to load-test a Moodle cluster on Azure using the Azure Resource Manager templates in this repository. ## Prerequisites To run load tests using the resources in this directory, you'll want to spin up a VM to manage the [Load Test](Deploy_Load_Test_VM.md) in your Azure subscription. This VM will generate the traffic, running it in Azure will minimize network charges. ## Deploying Moodle using templates and running load test Once dependencies are installed, you can initiate the load testing process by using included utility scripts. These scripts will: * deploy a Moodle cluster * set up the test course in Moodle * enrol students for the course * run a synthetic test workload using jMeter * teardown the test cluster Use the function `deploy_run_test1_teardown` to perform all these steps. This function takes 18 parameters in the following order: See the included example `run_load_test_example` in `loadtest/loadtest.sh`. At the time of writing this example is configured as follows: ``` bash ssh $ipAddress "az login --username $AZURE_LOGIN --password $AZURE_PASSWORD; az account set --subscription $AZURE_SUBSCRIPTION_ID; run_load_test_example" ``` Running this example will deploy a cluster with the following configuration: * Apache web server * Standard_DS2_v2 Azure VM SKU * mysql database (with 200 DTU and 125GB DB size) * NFS file share (with 2 disks and 128GB disk size each) * uses your SSH pub key in `~/.ssh/id_rsa` [NOTE ON SSH KEYS] Ensure your `~/.ssh/id_rsa` has been added to ssh-agent using `eval $(ssh-agent)` and `ssh-add`). Once the Moodle cluster is deployed and configured with course and student data (using [moosh](https://moosh-online.com/) it will run the synthetic workload with designated number of concurrent threads (in the example we use 1600 thread) for the designated duration and rampup time (18000 seconds = 5 hours duration, 4800 seconds rampup time in the example). ## Test plans We'd like to offer test plans that are as realistic as possible, so that potential Moodle users on Azure can have better confidence with their Moodle deployment on Azure. We are just starting out in that direction and here are descriptions of currently available test plans. ### Simple Scenario [simple-test-1.jmx](./simple-test-1.jmx) This test plan is a simple scenario that performs the following operations repeatedly: * Login to the Moodle site * View the test course * View any resource if any * View a forum in the test course * View any forum discussion * Post a new discussion topic * Take a quiz and submit The scripts in [loadtest.sh](./loadtest.sh) are tailored for this test plan. ### Moodle Data Stress Testing [simple-test-2.jmx](./simple-test-2.jmx) Currently [loadtest.sh](./loadtest.sh) doesn't have any tailored scripts for this test plan. Therefore, this test plan will need to be executed by issuing the actual jmeter command with properly modified parameters manually, or it'd be greatly appreciated if someone can contribute better support for this test plan in [loadtest.hs](./loadtest.sh). The purpose of this test plan is to try stressing the moodledata directory in a shared file system (either a gluster volume or an NFS share, depending on the choice). Initially attaching a random file in a forum discussion post was tried, but for some reason (probably due to my lack of understanding in PHP/web interaction), files were not attached. I instead tried to upload random files to each test Moodle users's Private Files area, and it did work. This test plan basically performs the following operations repeatedly: * Login to the Moodle site * Open the Moodle user's Private Files repository * Upload a randomly generated file (of a random size within a hard-coded range) * Save the change This way, we were able to populate the shared moodledata directory with random files in Moodle users' Private Files repositories. The mechanism to generate random files is not so efficient, so that's currently what slows down the upload speed, and any improvement in that BeanShell preprocessor code would be great. Note that the uploaded files have to be different. Moodle seems so good at deduplicating that a single file uploaded multiple times by different users won't increase the file system usage beyond its single copy. It'd be also great if we add a download operation step in the test plan, and it's left as a future work item. ### Latency-Sensitive Stress Testing [time-gated-exam-test.jmx](./time-gated-exam-test.jmx) This test stresses the deployed Moodle cluster with 1000 emulated students trying to get in an exam (quiz) that's initially closed and will be opened at the designated exam start time (have to be manually set on the test course's corresponding quiz's Settings). Once the exam start time passes, each emulated student continues taking the exam for 10 times. This test has been used to find out how responsive a deployed Moodle cluster can be on very latency-sensitive workloads. We've been using this test with different file server types to find out which file server type offers best response times. ## Please contribute It'd be great if we have more test plans, and make other parameters configurable (for example, make the auto-scaling thresholds configurable, which actually requires some changes in the templates as well). The currently available test plans also have hard-coded database type (JDBC connection string) that won't work for Postgres SQL server, so making it work would be also greatly appreciated. Also, if you run this load test with any parameters, it'd be great to share the numeric results so that we can have more performance data on various configurations. Here is [a link to an Excel spreadsheet](https://1drv.ms/x/s!Aj6KpM6lFGAjgd4D6IV8_6M42q9omA) where anyone can share their load testing results. ## Acknowledgement The original test course and the test plan were generously provided by [Catalyst](https://github.com/catalyst) as part of this template modernization project. jMeter is a great load testing tool, and also thanks to [moosh](http://moosh-online.com/), the whole process could be automated without too much difficulty, which was really nice. ================================================ FILE: loadtest/azuredeploy.parameters.loadtest.defaults.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "autoscaleVmSku": { "value": "__WEB_VM_SKU__" }, "dbServerType": { "value": "__DB_SERVER_TYPE__" }, "mysqlPgresVcores": { "value": 2 }, "mysqlPgresStgSizeGB": { "value": 125 }, "webServerType": { "value": "__WEB_SERVER_TYPE__" }, "fileServerType": { "value": "__FILE_SERVER_TYPE__" }, "fileServerDiskCount": { "value": 2 }, "fileServerDiskSize": { "value": 128 }, "redisDeploySwitch": { "value": false }, "sshPublicKey": { "value": "__SSH_PUB_KEY__" } } } ================================================ FILE: loadtest/loadtest.sh ================================================ #!/bin/bash # This is not fully tested. Just documenting what's needed. function install_java_and_jmeter { sudo apt update || return 1 sudo apt install -y openjdk-8-jdk || return 1 wget -O apache-jmeter-4.0.tgz https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-4.0.tgz || return 1 tar xfz apache-jmeter-4.0.tgz -C ~ mkdir -p ~/bin ln -s ~/apache-jmeter-4.0/bin/jmeter ~/bin/jmeter rm apache-jmeter-4.0.tgz wget -O mysql-connector-java-5.1.45.tar.gz https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.45.tar.gz || return 1 tar xfz mysql-connector-java-5.1.45.tar.gz mv mysql-connector-java-5.1.45/mysql-connector-java-5.1.45-bin.jar ~/apache-jmeter-4.0/lib rm -rf mysql-connector-java-5.1.45* wget -O postgres-42.2.1.jar https://jdbc.postgresql.org/download/postgresql-42.2.1.jar || return 1 mv postgres-42.2.1.jar ~/apache-jmeter-4.0/lib # Have to have jmeter plugins manager and have it download the needed plugins in advance... wget -O jmeter-plugins-manager-0.19.jar http://search.maven.org/remotecontent?filepath=kg/apc/jmeter-plugins-manager/0.19/jmeter-plugins-manager-0.19.jar || return 1 mv jmeter-plugins-manager-0.19.jar ~/apache-jmeter-4.0/lib/ext wget -O cmdrunner-2.0.jar http://search.maven.org/remotecontent?filepath=kg/apc/cmdrunner/2.0/cmdrunner-2.0.jar || return 1 mv cmdrunner-2.0.jar ~/apache-jmeter-4.0/lib java -cp ~/apache-jmeter-4.0/lib/ext/jmeter-plugins-manager-0.19.jar org.jmeterplugins.repository.PluginManagerCMDInstaller # TODO Hard-coded .jmx file here. Do this for each individual .jmx file wget -O tmp-for-plugin-install.jmx https://raw.githubusercontent.com/Azure/Moodle/master/loadtest/simple-test-1.jmx || return 1 ~/apache-jmeter-4.0/bin/PluginsManagerCMD.sh install-for-jmx tmp-for-plugin-install.jmx rm tmp-for-plugin-install.jmx } function install_az_cli { local az_repo=$(lsb_release -cs) sudo mkdir -p /etc/apt/keyrings curl -sLS https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/keyrings/microsoft.gpg > /dev/null sudo chmod go+r /etc/apt/keyrings/microsoft.gpg echo "deb [arch=`dpkg --print-architecture` signed-by=/etc/apt/keyrings/microsoft.gpg] https://packages.microsoft.com/repos/azure-cli/ $az_repo main" | sudo tee /etc/apt/sources.list.d/azure-cli.list export DEBIAN_FRONTEND=noninteractive sudo apt-get -qq -o=Dpkg::Use-Pty=0 update || return 1 sudo apt-get --yes --no-install-recommends \ -qq -o=Dpkg::Use-Pty=0 \ -o Dpkg::Options::="--force-confdef" \ -o Dpkg::Options::="--force-confold" \ install azure-cli || return 1 } function check_if_logged_on_azure { az account show --query id -o tsv > /dev/null 2>&1 if [ $? != "0" ]; then echo "Not logged on to Azure. Run 'az login' first and make sure subscription is set to your desired one." return 1 fi } function show_command_to_run { echo "Running command: $*" } function check_db_sku_params { local vcores=${1} local size=${2} # In GB if [ "$vcores" != 1 -a "$vcores" != 2 -a "$vcores" != 4 -a "$vcores" != 8 -a "$vcores" != 16 -a "$vcores" != 32 ]; then echo "Invalid vCores ($vcores). Only allowed are 1, 2, 4, 8, 16, 32." return 1 fi if [ -z "${size##*[!0-9]*}" ] || [ "$size" -lt 5 ] || [ "$size" -gt 1024 ]; then echo "Invalid DB size ($size). Only allowed are 5, 6, 7, ..., 1024." return 1 fi # TODO Add other SKU params: Tiers (Basic/GeneralPurpose/MemoryOptimized), HW family (Gen4/Gen5) } # TODO hard-coded Azure location in global variable. Parametrize this later. MOODLE_RG_LOCATION=southcentralus function deploy_moodle_with_some_parameters { check_if_logged_on_azure || return 1 local resource_group=${1} # Azure resource group where templates will be deployed local template_url=${2} # Github URL of the top template to deploy local parameters_template_file=${3} # Local parameter template file local web_server_type=${4} # E.g., apache or nginx local web_vm_sku=${5} # E.g., Standard_DS2_v2 local db_server_type=${6} # E.g., mysql or postgres local db_vcores=${7} # 1, 2, 4, 8, 16, 32 only local db_size_gb=${8} # 5 to 1024, integer only local file_server_type=${9} # E.g., nfs or gluster local file_server_disk_count=${10} # 2, 3, 4 local file_server_disk_size=${11} # in GB local redis_cache=${12} # Redis cache choice. Currently 'true' or 'false' only. local ssh_pub_key=${13} # Your ssh authorized_keys content local no_wait_flag=${14} # Must be "--no-wait" to be passed to az check_db_sku_params $db_vcores $db_size_gb || return 1 local cmd="az group create --resource-group $resource_group --location $MOODLE_RG_LOCATION" show_command_to_run $cmd eval $cmd || return 1 local deployment_name="${resource_group}-deployment" local cmd="az deployment group create --resource-group $resource_group --name $deployment_name $no_wait_flag --template-uri $template_url --parameters @$parameters_template_file webServerType=$web_server_type autoscaleVmSku=$web_vm_sku dbServerType=$db_server_type mysqlPgresVcores=$db_vcores mysqlPgresStgSizeGB=$db_size_gb fileServerType=$file_server_type fileServerDiskCount=$file_server_disk_count fileServerDiskSize=$file_server_disk_size redisDeploySwitch=$redis_cache sshPublicKey='$ssh_pub_key'" show_command_to_run $cmd eval $cmd } function delete_resource_group { check_if_logged_on_azure || return 1 local resource_group=${1} local cmd="az group delete --resource-group $resource_group" show_command_to_run $cmd eval $cmd } function install_moosh { # 'composer install' keeps failing, so try apt... sudo apt-add-repository "deb http://ppa.launchpad.net/zabuch/ppa/ubuntu $(lsb_release -sc) main" sudo apt-get update || true sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CA1F0167ECFEA95 sudo apt-get install --assume-yes moosh # sudo apt update || return 1 # sudo apt install -y composer || return 1 # cd ~ # git clone git://github.com/tmuras/moosh.git || return 1 # cd moosh # composer install || sleep 30 && composer install || sleep 30 && composer install || return 1 # mkdir -p ~/bin # ln -s $PWD/moosh.php ~/bin/moosh } MOODLE_PATH=/moodle/html/moodle function delete_course { local course_id=${1} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-delete $course_id } function create_course { local course_id=${1} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-create --idnumber=$course_id empty@test.course } function restore_course_from_url { local url=${1} wget $url -O backup_to_restore.mbz sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-restore backup_to_restore.mbz 1 } function create_2000_test_users_and_enroll_them_in_course { local course_id=${1} local password=${2} # TODO ugly... sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{1..200} sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{201..400} sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{401..600} sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{601..800} sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{801..1000} sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{1001..1200} sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{1201..1400} sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{1401..1600} sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{1601..1800} sudo -u www-data moosh --moodle-path=$MOODLE_PATH user-create -p $password m_azuretestuser_{1801..2000} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{1..200} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{201..400} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{401..600} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{601..800} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{801..1000} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{1001..1200} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{1201..1400} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{1401..1600} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{1601..1800} sudo -u www-data moosh --moodle-path=$MOODLE_PATH course-enrol $course_id m_azuretestuser_{1801..2000} } function hide_course_overview_block_for_jmeter_test { # "myoverview" is the registered name of the "Course overview" block sudo -u www-data moosh --moodle-path=$MOODLE_PATH block-manage hide myoverview } # TODO hard-coded values... LOADTEST_BASE_URI=https://raw.githubusercontent.com/Azure/Moodle/master/loadtest MOODLE_TEST_USER_PASSWORD='testUserP@$$w0rd' function setup_test_course_and_users { install_moosh # TODO hard-coded test course backup location restore_course_from_url $LOADTEST_BASE_URI/moodle-on-azure-test-course-1.mbz local course_id=2 # TODO Fix this hard-coded course id #. Should be retrieved from the previous restore_course_from_url output local password=$MOODLE_TEST_USER_PASSWORD # TODO parameterize create_2000_test_users_and_enroll_them_in_course $course_id $password hide_course_overview_block_for_jmeter_test } function run_cmd_on_remote_host { local func_cmd=${1} # E.g., install_moosh or 'delete_course 2' local ssh_dest=${2} # E.g., azureadmin@10.2.3.4 local port=${3:-22} # E.g., 2222 local cmd="ssh -o 'StrictHostKeyChecking no' -p $port $ssh_dest 'wget $LOADTEST_BASE_URI/loadtest.sh -O loadtest.sh; source loadtest.sh; $func_cmd'" show_command_to_run $cmd eval $cmd } function run_simple_test_1_on_resource_group { local resource_group=${1} # Azure resource group where Moodle templates were deployed local test_threads_count=${2} # E.g., 400, 800, ... local test_rampup_time_sec=${3} # E.g., 900 (should be long enough for # threads above) local test_run_time_sec=${4} # E.g., 3600 for 1 hour local setup_test_course_users_flag=${5} # Run setup_test_course_and_users on moodle_host if nonzero sudo apt update; sudo apt install -y jq local deployment="${resource_group}-deployment" local output=$(az deployment group show -g $resource_group -n $deployment) local moodle_host=$(echo $output | jq -r .properties.outputs.siteURL.value) local db_host=$(echo $output | jq -r .properties.outputs.databaseDNS.value) local moodle_db_user=$(echo $output | jq -r .properties.outputs.moodleDbUsername.value) local moodle_db_pass=$(echo $output | jq -r .properties.outputs.moodleDbPassword.value) local moodle_user_pass=$MOODLE_TEST_USER_PASSWORD # TODO parameterize if [ -n "$setup_test_course_users_flag" ]; then local moodle_controller_ip=$(echo $output | jq -r .properties.outputs.controllerInstanceIP.value) run_cmd_on_remote_host setup_test_course_and_users azureadmin@${moodle_controller_ip} fi mkdir -p test_outputs local prefix="test_outputs/simple_test_1_$(date +%Y%m%d%H%M%S)" echo $output | jq . > ${prefix}.deployment.json export JVM_ARGS="-Xms1024m -Xmx4096m" local cmd="jmeter -n -t simple-test-1.jmx -l ${prefix}.jmeter.results.txt -j ${prefix}.jmeter.log -e -o ${prefix}.jmeter.report -Jhost=${moodle_host} -Jdb_host=${db_host} -Jdb_user=${moodle_db_user} '-Jdb_pass=${moodle_db_pass}' '-Jmoodle_user_pass=${moodle_user_pass}' -Jthreads=${test_threads_count} -Jrampup=${test_rampup_time_sec} -Jruntime=${test_run_time_sec}" show_command_to_run $cmd eval $cmd } function deallocate_services_in_resource_group { local rg=${1} # Deallocate VMSS's local scalesets=$(az vmss list -g $rg --query [].name -o tsv) for scaleset in $scalesets; do local cmd="az vmss deallocate -g $rg --name $scaleset" show_command_to_run $cmd eval $cmd done # Deallocate VMs local cmd="az vm deallocate --ids $(az vm list -g $rg --query [].id -o tsv)" show_command_to_run $cmd eval $cmd # Stopping DBs and redis cache is currently not possible on Azure. } function deploy_run_test1_teardown { local resource_group=${1} local location=${2} local template_url=${3} local parameters_template_file=${4} local web_server_type=${5} local web_vm_sku=${6} local db_server_type=${7} local db_vcores=${8} local db_size_gb=${9} local file_server_type=${10} local file_server_disk_count=${11} local file_server_disk_size=${12} local redis_cache=${13} local ssh_pub_key=${14} local test_threads_count=${15} local test_rampup_time_sec=${16} local test_run_time_sec=${17} local delete_resource_group_flag=${18} # Any non-empty string is considered true MOODLE_RG_LOCATION=$location deploy_moodle_with_some_parameters $resource_group $template_url $parameters_template_file $web_server_type $web_vm_sku $db_server_type $db_vcores $db_size_gb $file_server_type $file_server_disk_count $file_server_disk_size $redis_cache "$ssh_pub_key" || return 1 run_simple_test_1_on_resource_group $resource_group $test_threads_count $test_rampup_time_sec $test_run_time_sec 1 || return 1 if [ -n "$delete_resource_group_flag" ]; then az group delete -g $resource_group -y else deallocate_services_in_resource_group $resource_group fi } function check_ssh_agent_and_added_key { ssh-add -l if [ $? != "0" ]; then echo "No ssh key added to ssh-agent or no ssh-agent is running. Make sure to run ssh-agent (eval `ssh-agent`) and add the correct ssh key (usually just ssh-add will do), so that remote commands execution through ssh doesn't prompt for interactive password." return 1 fi } function run_load_test_example { check_ssh_agent_and_added_key || return 1 deploy_run_test1_teardown ltest6 southcentralus https://raw.githubusercontent.com/Azure/Moodle/master/azuredeploy.json azuredeploy.parameters.loadtest.defaults.json apache Standard_DS2_v2 mysql 4 125 nfs 2 128 false "$(cat ~/.ssh/authorized_keys)" 1600 4800 18000 } function run_load_test_postgres { check_ssh_agent_and_added_key || return 1 deploy_run_test1_teardown pgres southcentralus https://raw.githubusercontent.com/Azure/Moodle/master/azuredeploy.json azuredeploy.parameters.loadtest.defaults.json apache Standard_DS2_v2 postgres 16 256 nfs 2 128 false "$(cat ~/.ssh/authorized_keys)" 800 2400 36000 } ================================================ FILE: loadtest/simple-test-1.jmx ================================================ false false login_host ${__P(host, lb-cosbae.southcentralus.cloudapp.azure.com)} = host ${__P(host, lb-cosbae.southcentralus.cloudapp.azure.com)} = threads ${__P(threads,400)} = Default 10 (for a quick GUI run) rampup ${__P(rampup,900)} = Default 120 - 1/15 runtime default rampup runtime ${__P(runtime,10800)} = Default 1800 = 30 minutes = 30*60 seconds default_connect_timeout 2000 = 1.2 seconds to a TCP ack default_response_timeout 240000 = 4 Minutes for the page to respond throughput_rate ${__P(throughput_rate, 50)} = Default 650 should approximate ~ 7k page views / 5 mins cacti sample activity_course_id ${__P(activity_course_id, 2)} = This must match the course ID of the course with performance activities quiz_delay ${__P(quiz_delay, 100)} -1 means don't run quiz = quiz_users ${__P(quiz_users, 1)} Number of quiz users to simulate = groupselect_delay ${__P(groupselect_delay,-1)} = groupselect_users ${__P(groupselect_users, 0)} = protocol https = port 443 = moodle_user_pass ${__P(moodle_user_pass,testUserP@$$w0rd)} = startnextloop false -1 ${threads} ${rampup} 1513564238000 1513564538000 true ${runtime} 0 true default User-Agent Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; InfoPath.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; OfficeLiveConnector.1.5; OfficeLivePatch.1.3) Accept */* Accept-Language en-us true VIEWSTATE jsessionid RandomNumber username userRandomChoice sesskey testBigFileLocation testBigFileName discussionid courseList xx_outputFromSplit ${__Random(1,2000,ReuseRandomNumber)} m_azuretestuser_${RandomNumber} ${RandomNumber} ${sesskey} ${__V(course_list_${RandomNumber})} ${__split(${courseList}, course_id,|)} true ${host} ${port} ${protocol} / GET true false true false Java ${default_connect_timeout} ${default_response_timeout} 1000 8000 4000 true ${username} username = true true true ${moodle_user_pass} password = true true true Login = true Login ${login_host} ${port} ${protocol} /login/index.php POST true false true false true Java ${default_connect_timeout} ${default_response_timeout} You are logged in as 2 Assertion.response_data false sesskey //*[@name='sesskey'][1]/@value false true false -1 1000 5000 0 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true true 5 RandomCourse courseid ${__Random(1,${course_id_n},ReuseRandomNumber)} ${activity_course_id} true false ${courseid} id = true true ${host} ${port} ${protocol} /course/view.php GET true false true false Java ${default_connect_timeout} ${default_response_timeout} 1000 10000 25000 false resource_module_id \/mod\/resource\/view.php\?id=(\d+)" $1$ NULL 0 ${__Random(1,10,ReuseRandomNumber)} <= 10 false "${resource_module_id}" != "NULL" false false ${resource_module_id} id = true true ${host} ${port} ${protocol} /mod/resource/view.php GET true false true false Java 1000 3000 5000 false true false false saveConfig true true true true true true true false true true false false false false false false false false 0 true true true 3 20% of the time ${__Random(1,10,ReuseRandomNumber)} <= 10 false false ${activity_course_id} id = true true ${host} ${port} ${protocol} /course/view.php GET true false true false Java Performance Testing Forum 2 Assertion.response_data false false coursetopics_html Performance Testing Peak Quiz.*(<div><div class="mod-indent-outer">.*?<a .*Performance Testing Forum 0.*Performance Testing Forum 9.*?<\/a>) $1$ NULL NULL moduleid substring-after(//*[contains(text(),'Performance Testing Forum ${__Random(0,9,ReuseRandomNumber)}')]/../@href,'id=') false true false true false Pick one of the 10 Random forums, we use 10 to reduce the index impact of one becoming large and exploding the query time to view the forum true variable coursetopics_html -1 NULL moduleid substring-after(//*/div[@class="activityinstance"]/*/*[contains(text(),'Performance Testing Forum ${__Random(0,9,ReuseRandomNumber)}')]/../@href,'id=') false true false true false Pick one of the 10 Random forums, we use 10 to reduce the index impact of one becoming large and exploding the query time to view the forum true -1 1000 10000 30000 false ${moduleid} id = true true ${host} ${port} ${protocol} /mod/forum/view.php GET true false true false Java Performance Testing Forum Add a new discussion topic Assertion.response_data false 2 false forumheaderlist (?s)(<table .*?class="forumheaderlist".*?<\/table>) $1$ NULL NULL discuss_id substring-after(//*/div[@class="activityinstance"]/*/*[contains(text(),'Performance Testing Forum ${__Random(0,9,ReuseRandomNumber)}')]/../@href,'id=') false true false true false Pick one of the 10 Random forums, we use 10 to reduce the index impact of one becoming large and exploding the query time to view the forum true variable forumheaderlist -1 false forumid name="forum" value="(\d+)" $1$ NULL forumid //*[@name='forum'][1]/@value false true false true true false -1 false discuss_id \/mod\/forum\/discuss.php\?d=(\d+) $1$ NULL 0 false discuss_title \/mod\/forum\/discuss.php\?d=${discuss_id}">(.*?)</a> $1$ NULL 1 1000 25000 20000 false true false false saveConfig true true true true true true true false true true false false false false false false false false 0 true true "${discuss_id}" != "NULL" && ${discuss_id} > 0 false ${__Random(1,10,ReuseRandomNumber)} <= 10 false false ${discuss_id} d = true true ${host} ${port} ${protocol} /mod/forum/discuss.php GET true false true false Java View a forum discussion thread ${discuss_title} My Course list is Assertion.response_data false 2 false forumid name="forum" value="(\d+)" $1$ NULL forumid //*[@name='forum'][1]/@value false true false true true false -1 1000 3000 12000 false ${discuss_id} d = true true ${host} ${port} ${protocol} /mod/forum/discuss.php GET true false true false Java View a forum discussion thread ${discuss_title} My Course list is Assertion.response_data false 2 false forumid name="forum" value="(\d+)" $1$ NULL forumid //*[@name='forum'][1]/@value false true false true true false -1 1000 10000 4000 Only create a forum post (10% of the time a forum is viewed) ${__Random(1,20,ReuseRandomNumber)} <= 20 false false ${forumid} forum = true true ${host} ${port} ${protocol} /mod/forum/post.php GET true false true false Java Your new discussion topic Assertion.response_data false 2 discussionid //*[@name='discussion'][1]/@value false true false true true false -1 false discussionid name="discussion".*?value="([\d]+)" $1$ NULL 1 parentid //*[@name='parent'][1]/@value false true false true true false -1 false parentid name="parent".*?value="([\d]+)" $1$ NULL 1 userid //*[@name='userid'][1]/@value false true false true true false -1 false userid name="userid".*?value="([\d]+)" $1$ NULL 1 groupid //*[@name='groupid'][1]/@value false true false true true false -1 false groupid name="groupid".*?value="([\d]+)" $1$ NULL 1 editid //*[@name='edit'][1]/@value false true false true true false -1 false editid name="edit".*?value="([\d]+)" $1$ NULL 1 attachmentid //*[@name='attachment'][1]/@value false true false true true false -1 false attachmentid name="attachment".*?value="([\d]+)" $1$ NULL 1 1000 4000 3000 messageitemid //*[@name='message[itemid]'][1]/@value false true false true true false -1 false messageitemid name="message\[itemid\]".*?value="([\d]+)" $1$ NULL 1 false true false false saveConfig true true true true true true true false true true false false false false false false false false 0 true true true 1 _qf__mod_forum_post_form = true true false ${attachmentid} = true attachments true ${activity_course_id} course = true true true ${discussionid} discussion = true true true ${editid} edit = true true true ${forumid} forum = true true true ${groupid} groupid = true true false 1 = true message[format] false ${messageitemid} = true message[itemid] true <p>My Course list is ${courseList}</p> message[text] = true true true ${parentid} parent = true true true 0 reply = true true true ${sesskey} sesskey = true true true ${username} - ${RandomNumber} subject = true true false Post to forum = true submitbutton true 0 timestart = true true true 0 timeend = true true true ${userid} userid = true true ${host} ${port} ${protocol} /mod/forum/post.php POST true false true true Java Your post was successfully 2 Assertion.response_data false 1000 false ${moduleid} id = true true ${host} ${port} ${protocol} /mod/forum/view.php GET true false true false Java Performance Testing Forum Assertion.response_data false 2 discussionid substring-after(//*/a[contains(text(),'${username} - ${RandomNumber}')][1]/@href,'=') false true false true true false -1 false discussionid name="message\[itemid\]".*?value="([\d]+)" $1$ NULL 1 1000 4000 3000 false true false false saveConfig true true true true true true true false true true false false false false false false false false 0 true true false saveConfig true true true true true true true false true true false false true false false false false false 0 true true ${__Random(1,10,ReuseRandomNumber)} <= 10 false 30% of the time, do the quiz false ${activity_course_id} id = true true ${host} ${port} ${protocol} /course/view.php GET true false true false Java Performance Testing Regular Quiz 2 Assertion.response_data false false classtopics_html (?s)(<ul class="topics".*?)<\/div>.*?<\/div>.*?<\/div>.*?<\/div>.*?<\/div>.*?<div id="region-pre" $1$ NULL false html_chunk_perfquiz (.{0,250}Performance Testing Regular Quiz) $1$ NULL false moduleid \/mod\/quiz\/view\.php\?id=(\d+) $1$ NULL variable html_chunk_perfquiz NULL moduleid substring-after(//*[contains(text(),'Performance Testing Regular Quiz')]/../@href,'id=') false true false true false true variable classtopics_html -1 moduleid substring-after(//*/a/*[contains(text(),'Performance Testing Regular Quiz')]/../@href,'=') false true false true true false -1 4000 3000 1000 false ${moduleid} id = true true ${host} ${port} ${protocol} /mod/quiz/view.php GET true false true false Java Performance Testing Regular Quiz Assertion.response_data false 2 quizid //*/form[@action="https://${host}/mod/quiz/startattempt.php"]//*/input[@name='cmid'][1]/@value false true false true true false -1 10000 3000 1000 false ${moduleid} cmid = true true false ${sesskey} = true sesskey ${host} ${port} ${protocol} /mod/quiz/startattempt.php POST true false true false Java What's your username Is the LMS Performing acceptably? What is 2 \+ 2\? Assertion.response_data false 2 10000 3000 1000 attemptid //*/form[@action="https://${host}/mod/quiz/processattempt.php"]//*/input[@name='attempt'][1]/@value false true false true true false -1 false attemptid \/mod\/quiz\/attempt\.php\?attempt=(\d+)">Continue<\/a> $1$ NULL children false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false 0 = true q${attemptid}:1_:flagged false 1 = true q${attemptid}:1_:sequencecheck false ${username}@${user_type} = true q${attemptid}:1_answer false 0 = true q${attemptid}:2_:flagged false 1 = true q${attemptid}:2_:sequencecheck false ${__Random(0,1)} = true q${attemptid}:2_answer false 0 = true q${attemptid}:3_:flagged false 1 = true q${attemptid}:3_:sequencecheck false ${__Random(3,5)} = true q${attemptid}:3_answer false Next = true next false ${attemptid} = true attempt false 0 = true thispage false -1 = true nextpage false 0 = true timeup false ${sesskey} = true sesskey false = true scrollpos false 1,2,3 = true slots ${host} ${port} ${protocol} utf-8 /mod/quiz/processattempt.php POST true false true true Java Review of attempt 2 Assertion.response_data false 25000 15000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${sesskey} = true sesskey false = true slots false 0 = true timeup false 1 = true finishattempt false ${attemptid} = true attempt ${host} ${port} ${protocol} utf-8 /mod/quiz/processattempt.php POST true false true true Java Review of attempt 2 Assertion.response_data false 4000 3000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${moduleid} = true id ${host} ${port} ${protocol} /mod/quiz/view.php GET true false true true Java Summary of your previous attempts 2 Assertion.response_data false Highest grade 2 Assertion.response_data false 4000 3000 1000 false true false false saveConfig true true true true true true true false true true false false true false false false false false 0 true true false saveConfig true true true true true true true false true true false false false false false false false false 0 true true 20000 20000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 1000 false false false false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 1000 false false false false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 500 false false false ================================================ FILE: loadtest/simple-test-2.jmx ================================================ false false login_host ${__P(host, lb-hivmol.southcentralus.cloudapp.azure.com)} = host ${__P(host, lb-hivmol.southcentralus.cloudapp.azure.com)} = threads ${__P(threads,100)} = Default 10 (for a quick GUI run) rampup ${__P(rampup,1200)} = Default 120 - 1/15 runtime default rampup runtime ${__P(runtime,86400)} = Default 1800 = 30 minutes = 30*60 seconds default_connect_timeout 2000 = 1.2 seconds to a TCP ack default_response_timeout 240000 = 4 Minutes for the page to respond throughput_rate ${__P(throughput_rate, 50)} = Default 650 should approximate ~ 7k page views / 5 mins cacti sample activity_course_id ${__P(activity_course_id, 2)} = This must match the course ID of the course with performance activities quiz_delay ${__P(quiz_delay, 100)} -1 means don't run quiz = quiz_users ${__P(quiz_users, 1)} Number of quiz users to simulate = groupselect_delay ${__P(groupselect_delay,-1)} = groupselect_users ${__P(groupselect_users, 0)} = protocol https = port 443 = moodle_user_pass ${__P(moodle_user_pass,testUserP@$$w0rd)} = startnextloop false -1 ${threads} ${rampup} 1513564238000 1513564538000 true ${runtime} 0 true default User-Agent Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; InfoPath.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; OfficeLiveConnector.1.5; OfficeLivePatch.1.3) Accept */* Accept-Language en-us true VIEWSTATE jsessionid RandomNumber username userRandomChoice sesskey discussionid courseList xx_outputFromSplit ${__Random(1,2000,ReuseRandomNumber)} m_azuretestuser_${RandomNumber} ${RandomNumber} ${sesskey} ${__V(course_list_${RandomNumber})} ${__split(${courseList}, course_id,|)} true ${host} ${port} ${protocol} / GET true false true false Java ${default_connect_timeout} ${default_response_timeout} 1000 8000 4000 true ${username} username = true true true ${moodle_user_pass} password = true true true Login = true Login ${login_host} ${port} ${protocol} /login/index.php POST true false true false true Java ${default_connect_timeout} ${default_response_timeout} You are logged in as 2 Assertion.response_data false sesskey //*[@name='sesskey'][1]/@value false true false -1 1000 5000 0 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true true 5 RandomCourse courseid ${__Random(1,${course_id_n},ReuseRandomNumber)} ${activity_course_id} true false ${courseid} id = true true ${host} ${port} ${protocol} /course/view.php GET true false true false Java ${default_connect_timeout} ${default_response_timeout} 1000 10000 25000 false resource_module_id \/mod\/resource\/view.php\?id=(\d+)" $1$ NULL 0 ${__Random(1,10,ReuseRandomNumber)} <= 10 false "${resource_module_id}" != "NULL" false false ${resource_module_id} id = true true ${host} ${port} ${protocol} /mod/resource/view.php GET true false true false Java 1000 3000 5000 ${__Random(1,10,ReuseRandomNumber)} <= 10 false ${host} ${port} ${protocol} /user/files.php GET true false true false Java false upload_sesskey name="sesskey".*?value="([\w]+)" $1$ NULL 1 false upload_filemanager_id "itemid":([\d]+) $1$ NULL 1 false upload_clientid "client_id":"([\w]+)" $1$ NULL 1 false upload_ctxid ctx_id=([\d]+) $1$ NULL 1 1000 3000 5000 ${upload_test_filename} repo_upload_file application/octet-stream false = true title false ${username} = true author false allrightsreserved = true license false ${upload_filemanager_id} = true itemid false 8 = true repo_id false = true p false = true page false filemanager = true env false ${upload_sesskey} = true sesskey false ${upload_clientid} = true client_id false ${upload_filemanager_id} = true itemid false -1 = true maxbytes false -1 = true areamaxbytes false ${upload_ctxid} = true ctx_id false / = true savepath ${host} ${port} ${protocol} /repository/repository_ajax.php?action=upload POST true false true true Java 1000 3000 5000 false import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; // Create a temp file File myFile = File.createTempFile("upload-", "" ); // Generate Random length string and write to file FileUtils.writeStringToFile(myFile, RandomStringUtils.random( RandomUtils.nextInt( 500000, 5000000 ) ), "UTF-8" ); // Store file name in variable. vars.put( "upload_test_filename", myFile.getCanonicalPath() ); false import org.apache.commons.io.FileUtils; // Delete file and do not throw error FileUtils.deleteQuietly(new File( vars.get("upload_test_filename"))); true ${protocol}://${host}/user/files.php = true returnurl false ${upload_sesskey} = true sesskey false 1 = true _qf__user_files_form false ${upload_filemanager_id} = true files_filemanager false Save changes = true submitbutton ${host} ${port} ${protocol} /user/files.php POST true false true false Java 1000 3000 5000 false true false false saveConfig true true true true true true true false true true false false false false false false false false 0 true true true 3 20% of the time ${__Random(1,10,ReuseRandomNumber)} <= 10 false false ${activity_course_id} id = true true ${host} ${port} ${protocol} /course/view.php GET true false true false Java Performance Testing Forum 2 Assertion.response_data false false coursetopics_html Performance Testing Peak Quiz.*(<div><div class="mod-indent-outer">.*?<a .*Performance Testing Forum 0.*Performance Testing Forum 9.*?<\/a>) $1$ NULL NULL moduleid substring-after(//*[contains(text(),'Performance Testing Forum ${__Random(0,9,ReuseRandomNumber)}')]/../@href,'id=') false true false true false Pick one of the 10 Random forums, we use 10 to reduce the index impact of one becoming large and exploding the query time to view the forum true variable coursetopics_html -1 NULL moduleid substring-after(//*/div[@class="activityinstance"]/*/*[contains(text(),'Performance Testing Forum ${__Random(0,9,ReuseRandomNumber)}')]/../@href,'id=') false true false true false Pick one of the 10 Random forums, we use 10 to reduce the index impact of one becoming large and exploding the query time to view the forum true -1 1000 10000 30000 false ${moduleid} id = true true ${host} ${port} ${protocol} /mod/forum/view.php GET true false true false Java Performance Testing Forum Add a new discussion topic Assertion.response_data false 2 false forumheaderlist (?s)(<table .*?class="forumheaderlist".*?<\/table>) $1$ NULL NULL discuss_id substring-after(//*/div[@class="activityinstance"]/*/*[contains(text(),'Performance Testing Forum ${__Random(0,9,ReuseRandomNumber)}')]/../@href,'id=') false true false true false Pick one of the 10 Random forums, we use 10 to reduce the index impact of one becoming large and exploding the query time to view the forum true variable forumheaderlist -1 false forumid name="forum" value="(\d+)" $1$ NULL forumid //*[@name='forum'][1]/@value false true false true true false -1 false discuss_id \/mod\/forum\/discuss.php\?d=(\d+) $1$ NULL 0 false discuss_title \/mod\/forum\/discuss.php\?d=${discuss_id}">(.*?)</a> $1$ NULL 1 1000 25000 20000 false true false false saveConfig true true true true true true true false true true false false false false false false false false 0 true true "${discuss_id}" != "NULL" && ${discuss_id} > 0 false ${__Random(1,10,ReuseRandomNumber)} <= 10 false false ${discuss_id} d = true true ${host} ${port} ${protocol} /mod/forum/discuss.php GET true false true false Java View a forum discussion thread ${discuss_title} My Course list is Assertion.response_data false 2 false forumid name="forum" value="(\d+)" $1$ NULL forumid //*[@name='forum'][1]/@value false true false true true false -1 1000 3000 12000 false ${discuss_id} d = true true ${host} ${port} ${protocol} /mod/forum/discuss.php GET true false true false Java View a forum discussion thread ${discuss_title} My Course list is Assertion.response_data false 2 false forumid name="forum" value="(\d+)" $1$ NULL forumid //*[@name='forum'][1]/@value false true false true true false -1 1000 10000 4000 Only create a forum post (10% of the time a forum is viewed) ${__Random(1,20,ReuseRandomNumber)} <= 20 false false ${forumid} forum = true true ${host} ${port} ${protocol} /mod/forum/post.php GET true false true false Java Your new discussion topic Assertion.response_data false 2 discussionid //*[@name='discussion'][1]/@value false true false true true false -1 false discussionid name="discussion".*?value="([\d]+)" $1$ NULL 1 parentid //*[@name='parent'][1]/@value false true false true true false -1 false parentid name="parent".*?value="([\d]+)" $1$ NULL 1 userid //*[@name='userid'][1]/@value false true false true true false -1 false userid name="userid".*?value="([\d]+)" $1$ NULL 1 groupid //*[@name='groupid'][1]/@value false true false true true false -1 false groupid name="groupid".*?value="([\d]+)" $1$ NULL 1 editid //*[@name='edit'][1]/@value false true false true true false -1 false editid name="edit".*?value="([\d]+)" $1$ NULL 1 attachmentid //*[@name='attachment'][1]/@value false true false true true false -1 false attachmentid value="([\d]+)".*?name="attachments" $1$ NULL 1 false ctxid ctx_id=([\d]+) $1$ NULL 1 false clientid "client_id":"([0-9a-f]+)" $1$ NULL 1 1000 4000 3000 messageitemid //*[@name='message[itemid]'][1]/@value false true false true true false -1 false messageitemid name="message\[itemid\]".*?value="([\d]+)" $1$ NULL 1 true ${activity_course_id} course = true true true ${forumid} forum = true true true ${discussionid} discussion = true true true ${parentid} parent = true true true ${groupid} groupid = true true true ${editid} edit = true true true 0 reply = true true true ${sesskey} sesskey = true true true 1 _qf__mod_forum_post_form = true true false 1 = true mform_isexpanded_id_general false 0 = true mform_isexpanded_id_displayperiod false 0 = true mform_isexpanded_id_tagshdr true ${username} - ${RandomNumber} subject = true true true <p>My Course list is ${courseList}</p> message[text] = true true false 1 = true message[format] false ${messageitemid} = true message[itemid] false 1 = true discussionsubscribe false ${attachmentid} = true attachments false _qf__force_multiselect_submission = true tags false Post to forum = true submitbutton ${host} ${port} ${protocol} /mod/forum/post.php POST true false true true Java Your post was successfully 2 Assertion.response_data false 1000 false ${moduleid} id = true true ${host} ${port} ${protocol} /mod/forum/view.php GET true false true false Java Performance Testing Forum Assertion.response_data false 2 discussionid substring-after(//*/a[contains(text(),'${username} - ${RandomNumber}')][1]/@href,'=') false true false true true false -1 false discussionid name="message\[itemid\]".*?value="([\d]+)" $1$ NULL 1 1000 4000 3000 false true false false saveConfig true true true true true true true false true true false false false false false false false false 0 true true false saveConfig true true true true true true true false true true false false true false false false false false 0 true true ${__Random(1,10,ReuseRandomNumber)} <= 10 false 30% of the time, do the quiz false ${activity_course_id} id = true true ${host} ${port} ${protocol} /course/view.php GET true false true false Java Performance Testing Regular Quiz 2 Assertion.response_data false false classtopics_html (?s)(<ul class="topics".*?)<\/div>.*?<\/div>.*?<\/div>.*?<\/div>.*?<\/div>.*?<div id="region-pre" $1$ NULL false html_chunk_perfquiz (.{0,250}Performance Testing Regular Quiz) $1$ NULL false moduleid \/mod\/quiz\/view\.php\?id=(\d+) $1$ NULL variable html_chunk_perfquiz NULL moduleid substring-after(//*[contains(text(),'Performance Testing Regular Quiz')]/../@href,'id=') false true false true false true variable classtopics_html -1 moduleid substring-after(//*/a/*[contains(text(),'Performance Testing Regular Quiz')]/../@href,'=') false true false true true false -1 4000 3000 1000 false ${moduleid} id = true true ${host} ${port} ${protocol} /mod/quiz/view.php GET true false true false Java Performance Testing Regular Quiz Assertion.response_data false 2 quizid //*/form[@action="https://${host}/mod/quiz/startattempt.php"]//*/input[@name='cmid'][1]/@value false true false true true false -1 10000 3000 1000 false ${moduleid} cmid = true true false ${sesskey} = true sesskey ${host} ${port} ${protocol} /mod/quiz/startattempt.php POST true false true false Java What's your username Is the LMS Performing acceptably? What is 2 \+ 2\? Assertion.response_data false 2 10000 3000 1000 attemptid //*/form[@action="https://${host}/mod/quiz/processattempt.php"]//*/input[@name='attempt'][1]/@value false true false true true false -1 false attemptid \/mod\/quiz\/attempt\.php\?attempt=(\d+)">Continue<\/a> $1$ NULL children false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false 0 = true q${attemptid}:1_:flagged false 1 = true q${attemptid}:1_:sequencecheck false ${username}@${user_type} = true q${attemptid}:1_answer false 0 = true q${attemptid}:2_:flagged false 1 = true q${attemptid}:2_:sequencecheck false ${__Random(0,1)} = true q${attemptid}:2_answer false 0 = true q${attemptid}:3_:flagged false 1 = true q${attemptid}:3_:sequencecheck false ${__Random(3,5)} = true q${attemptid}:3_answer false Next = true next false ${attemptid} = true attempt false 0 = true thispage false -1 = true nextpage false 0 = true timeup false ${sesskey} = true sesskey false = true scrollpos false 1,2,3 = true slots ${host} ${port} ${protocol} utf-8 /mod/quiz/processattempt.php POST true false true true Java Review of attempt 2 Assertion.response_data false 25000 15000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${sesskey} = true sesskey false = true slots false 0 = true timeup false 1 = true finishattempt false ${attemptid} = true attempt ${host} ${port} ${protocol} utf-8 /mod/quiz/processattempt.php POST true false true true Java Review of attempt 2 Assertion.response_data false 4000 3000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${moduleid} = true id ${host} ${port} ${protocol} /mod/quiz/view.php GET true false true true Java Summary of your previous attempts 2 Assertion.response_data false Highest grade 2 Assertion.response_data false 4000 3000 1000 false true false false saveConfig true true true true true true true false true true false false true false false false false false 0 true true false saveConfig true true true true true true true false true true false false false false false false false false 0 true true 20000 20000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 1000 false false false false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 1000 false false false false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 500 false false false ================================================ FILE: loadtest/time-gated-exam-test-dist-slaves.jmx ================================================ false false login_host ${__P(host, lb-kkvgra.southcentralus.cloudapp.azure.com)} = host ${__P(host, lb-kkvgra.southcentralus.cloudapp.azure.com)} = threads ${__P(threads,250)} = Default 10 (for a quick GUI run) rampup ${__P(rampup,250)} = Default 120 - 1/15 runtime default rampup runtime ${__P(runtime,3600)} = Default 1800 = 30 minutes = 30*60 seconds default_connect_timeout 2000 = 1.2 seconds to a TCP ack default_response_timeout 240000 = 4 Minutes for the page to respond throughput_rate ${__P(throughput_rate, 50)} = Default 650 should approximate ~ 7k page views / 5 mins cacti sample activity_course_id ${__P(activity_course_id, 2)} = This must match the course ID of the course with performance activities quiz_delay ${__P(quiz_delay, 100)} -1 means don't run quiz = quiz_users ${__P(quiz_users, 1)} Number of quiz users to simulate = groupselect_delay ${__P(groupselect_delay,-1)} = groupselect_users ${__P(groupselect_users, 0)} = protocol https = port 443 = moodle_user_pass ${__P(moodle_user_pass,testUserP@$$w0rd)} = startnextloop false 1 ${threads} ${rampup} 1513564238000 1513564538000 true ${runtime} 0 true default User-Agent Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; InfoPath.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; OfficeLiveConnector.1.5; OfficeLivePatch.1.3) Accept */* Accept-Language en-us true VIEWSTATE jsessionid RandomNumber userNum username userRandomChoice sesskey testBigFileLocation testBigFileName discussionid courseList xx_outputFromSplit ${__Random(1,2000,ReuseRandomNumber)} ${__javaScript(Math.floor(ctx.getThreadGroup().getNumThreads()*props.getProperty("slaveNum"\,"0")+ctx.getThreadNum()+1).toFixed(0))} m_azuretestuser_${userNum} ${RandomNumber} ${sesskey} ${__V(course_list_${RandomNumber})} ${__split(${courseList}, course_id,|)} true ${host} ${port} ${protocol} / GET true false true false Java ${default_connect_timeout} ${default_response_timeout} 1000 8000 4000 true ${username} username = true true true ${moodle_user_pass} password = true true true Login = true Login ${login_host} ${port} ${protocol} /login/index.php POST true false true false true Java ${default_connect_timeout} ${default_response_timeout} You are logged in as 2 Assertion.response_data false sesskey //*[@name='sesskey'][1]/@value false true false -1 1000 5000 0 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true true 10 false ${activity_course_id} id = true true ${host} ${port} ${protocol} /course/view.php GET true false true false Java Performance Testing Regular Quiz 2 Assertion.response_data true false classtopics_html (?s)(<ul class="topics".*?)<\/div>.*?<\/div>.*?<\/div>.*?<\/div>.*?<\/div>.*?<div id="region-pre" $1$ NULL false html_chunk_perfquiz (.{0,250}Performance Testing Regular Quiz) $1$ NULL false moduleid \/mod\/quiz\/view\.php\?id=(\d+) $1$ NULL variable html_chunk_perfquiz NULL moduleid substring-after(//*[contains(text(),'Performance Testing Regular Quiz')]/../@href,'id=') false true false true false true variable classtopics_html -1 moduleid substring-after(//*/a/*[contains(text(),'Performance Testing Regular Quiz')]/../@href,'=') false true false true true false -1 2000 1000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true ${__javaScript("${quiz_opened}" != "This quiz opened at")} false ${moduleid} id = true true ${host} ${port} ${protocol} /mod/quiz/view.php GET true false true false Java false quiz_opened (This quiz opened at) $1$ true The quiz will not be available until Assertion.response_data false 2 quizid //*/form[@action="https://${host}/mod/quiz/startattempt.php"]//*/input[@name='cmid'][1]/@value false true false true true false -1 500 200 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${moduleid} cmid = true true false ${sesskey} = true sesskey ${host} ${port} ${protocol} /mod/quiz/startattempt.php POST true false true false Java What's your username Is the LMS Performing acceptably? What is 2 \+ 2\? Assertion.response_data false 2 2000 1000 1000 attemptid //*/form[@action="https://${host}/mod/quiz/processattempt.php"]//*/input[@name='attempt'][1]/@value false true false true true false -1 false attemptid \/mod\/quiz\/attempt\.php\?attempt=(\d+).*?">Continue<\/a> $1$ NULL children false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false 0 = true q${attemptid}:1_:flagged false 1 = true q${attemptid}:1_:sequencecheck false ${username}@${user_type} = true q${attemptid}:1_answer false 0 = true q${attemptid}:2_:flagged false 1 = true q${attemptid}:2_:sequencecheck false ${__Random(0,1)} = true q${attemptid}:2_answer false 0 = true q${attemptid}:3_:flagged false 1 = true q${attemptid}:3_:sequencecheck false ${__Random(3,5)} = true q${attemptid}:3_answer false Next = true next false ${attemptid} = true attempt false 0 = true thispage false -1 = true nextpage false 0 = true timeup false ${sesskey} = true sesskey false = true scrollpos false 1,2,3 = true slots ${host} ${port} ${protocol} utf-8 /mod/quiz/processattempt.php POST true false true true Java Summary of attempt 2 Assertion.response_data false 2000 1000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${sesskey} = true sesskey false = true slots false 0 = true timeup false 1 = true finishattempt false ${attemptid} = true attempt ${host} ${port} ${protocol} utf-8 /mod/quiz/processattempt.php POST true false true true Java Finish review 2 Assertion.response_data false 2000 1000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${moduleid} = true id ${host} ${port} ${protocol} /mod/quiz/view.php GET true false true true Java Summary of your previous attempts 2 Assertion.response_data false Highest grade 2 Assertion.response_data false 4000 3000 1000 false true false false saveConfig true true true true true true true false true true false false true false false false false false 0 true true false saveConfig true true true true true true true false true true false false false false false false false false 0 true true 2000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 1000 false false false false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 1000 false false false false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 500 false false false false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true ================================================ FILE: loadtest/time-gated-exam-test.jmx ================================================ false false login_host ${__P(host, lb-d5t45x.westeurope.cloudapp.azure.com)} = host ${__P(host, lb-d5t45x.westeurope.cloudapp.azure.com)} = threads ${__P(threads,1000)} = Default 10 (for a quick GUI run) rampup ${__P(rampup,1000)} = Default 120 - 1/15 runtime default rampup runtime ${__P(runtime,3600)} = Default 1800 = 30 minutes = 30*60 seconds default_connect_timeout 2000 = 1.2 seconds to a TCP ack default_response_timeout 240000 = 4 Minutes for the page to respond throughput_rate ${__P(throughput_rate, 50)} = Default 650 should approximate ~ 7k page views / 5 mins cacti sample activity_course_id ${__P(activity_course_id, 2)} = This must match the course ID of the course with performance activities quiz_delay ${__P(quiz_delay, 100)} -1 means don't run quiz = quiz_users ${__P(quiz_users, 1)} Number of quiz users to simulate = groupselect_delay ${__P(groupselect_delay,-1)} = groupselect_users ${__P(groupselect_users, 0)} = protocol https = port 443 = moodle_user_pass ${__P(moodle_user_pass,testUserP@$$w0rd)} = startnextloop false 1 ${threads} ${rampup} 1513564238000 1513564538000 true ${runtime} 0 true default User-Agent Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36 Accept */* Accept-Language en-us true VIEWSTATE jsessionid RandomNumber username userRandomChoice sesskey testBigFileLocation testBigFileName discussionid courseList xx_outputFromSplit ${__Random(1,2000,ReuseRandomNumber)} m_azuretestuser_${__threadNum} ${RandomNumber} ${sesskey} ${__V(course_list_${RandomNumber})} ${__split(${courseList}, course_id,|)} true ${host} ${port} ${protocol} / GET true false true false Java ${default_connect_timeout} ${default_response_timeout} 1000 8000 4000 ${login_host} ${port} ${protocol} /login/index.php GET true false true false Java ${default_connect_timeout} ${default_response_timeout} 1000 4000 1000 logintoken input[name=logintoken] value NULL false true ${username} username = true true true ${moodle_user_pass} password = true true false ${logintoken} = true logintoken ${login_host} ${port} ${protocol} /login/index.php POST true false true false true Java ${default_connect_timeout} ${default_response_timeout} You are logged in as 2 Assertion.response_data false sesskey //*[@name='sesskey'][1]/@value false true false -1 1000 5000 0 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true true 10 false ${activity_course_id} id = true true ${host} ${port} ${protocol} /course/view.php GET true false true false Java Performance Testing Regular Quiz 2 Assertion.response_data true false classtopics_html (?s)(<ul class="topics".*?)<\/div>.*?<\/div>.*?<\/div>.*?<\/div>.*?<\/div>.*?<div id="region-pre" $1$ NULL false html_chunk_perfquiz (.{0,250}Performance Testing Regular Quiz) $1$ NULL false moduleid \/mod\/quiz\/view\.php\?id=(\d+) $1$ NULL variable html_chunk_perfquiz NULL moduleid substring-after(//*[contains(text(),'Performance Testing Regular Quiz')]/../@href,'id=') false true false true false true variable classtopics_html -1 moduleid substring-after(//*/a/*[contains(text(),'Performance Testing Regular Quiz')]/../@href,'=') false true false true true false -1 2000 1000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true ${__javaScript("${quiz_opened}" != "This quiz opened at")} false ${moduleid} id = true true ${host} ${port} ${protocol} /mod/quiz/view.php GET true false true false Java false quiz_opened (This quiz opened at) $1$ true The quiz will not be available until Assertion.response_data false 2 quizid //*/form[@action="https://${host}/mod/quiz/startattempt.php"]//*/input[@name='cmid'][1]/@value false true false true true false -1 500 200 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${moduleid} cmid = true true false ${sesskey} = true sesskey ${host} ${port} ${protocol} /mod/quiz/startattempt.php POST true false true false Java What's your username Is the LMS Performing acceptably? What is 2 \+ 2\? Assertion.response_data false 2 2000 1000 1000 attemptid //*/form[@action="https://${host}/mod/quiz/processattempt.php"]//*/input[@name='attempt'][1]/@value false true false true true false -1 false attemptid \/mod\/quiz\/attempt\.php\?attempt=(\d+).*?">Continue<\/a> $1$ NULL children false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false 0 = true q${attemptid}:1_:flagged false 1 = true q${attemptid}:1_:sequencecheck false ${username}@${user_type} = true q${attemptid}:1_answer false 0 = true q${attemptid}:2_:flagged false 1 = true q${attemptid}:2_:sequencecheck false ${__Random(0,1)} = true q${attemptid}:2_answer false 0 = true q${attemptid}:3_:flagged false 1 = true q${attemptid}:3_:sequencecheck false ${__Random(3,5)} = true q${attemptid}:3_answer false Next = true next false ${attemptid} = true attempt false 0 = true thispage false -1 = true nextpage false 0 = true timeup false ${sesskey} = true sesskey false = true scrollpos false 1,2,3 = true slots ${host} ${port} ${protocol} utf-8 /mod/quiz/processattempt.php POST true false true true Java Summary of attempt 2 Assertion.response_data false 2000 1000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${sesskey} = true sesskey false = true slots false 0 = true timeup false 1 = true finishattempt false ${attemptid} = true attempt ${host} ${port} ${protocol} utf-8 /mod/quiz/processattempt.php POST true false true true Java Finish review 2 Assertion.response_data false 2000 1000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false ${moduleid} = true id ${host} ${port} ${protocol} /mod/quiz/view.php GET true false true true Java Summary of your previous attempts 2 Assertion.response_data false Highest grade 2 Assertion.response_data false 4000 3000 1000 false true false false saveConfig true true true true true true true false true true false false true false false false false false 0 true true false saveConfig true true true true true true true false true true false false false false false false false false 0 true true 2000 1000 false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 1000 false false false false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 1000 false false false false saveConfig true true true true true true true false true true false false false true false false false true 0 true true true true true 500 false false false ================================================ FILE: managedApplication/Cleanup.md ================================================ # Cleaning up a Test Deployment If you worked through the documentation in this section you will have created a nubmer of resources and at least one entry into your Service Catalog. This document will explain how to remove them all. ## Prerequisites We need to ensure the [variables](Environment.md) are set up correctly. ## Microsoft Entra ID ``` bash MOODLE_MANAGED_APP_AD_ID=$(az ad group list --filter="displayName eq '$MOODLE_MANAGED_APP_OWNER_GROUP_NAME'" --query [0].objectId --output tsv) az ad group delete --group $MOODLE_MANAGED_APP_AD_ID ``` ## Remove the Service Catalog Entry ``` bash az managedapp definition delete --resource-group $MOODLE_SERVICE_CATALOG_RG_NAME --ids $MOODLE_MANAGED_APP_ID ``` ### Service catalog resource group If you create a resource group solely for the managed application you are now deleting you can safely remove its resource group: ``` bash az group delete --name $MOODLE_SERVICE_CATALOG_RG_NAME --yes ``` ## Managed Application By deleting the managed application Azure will automatically delete the managed application infrastructure resource group as well (this was created as part of the managed application deployment). First we need the application ID. ``` bash MOODLE_DEPLOYMENT_ID=$(az managedapp show --resource-group $MOODLE_DEPLOYMENT_RG_NAME --name $MOODLE_DEPLOYMENT_NAME) ``` Now we have the ID we can delete the application. ``` bash az managedapp delete --resource-group $MOODLE_DEPLOYMENT_RG_NAME --ids $MOODLE_DEPLOYMENT_ID ``` ================================================ FILE: managedApplication/DeployMoodleManagedApp.md ================================================ # Deploy a Moodle Based Managed Application into a Customer's Subscription In this tutorial we'll demonstrate how your customers will deploy an instance of your Moodle Based Managed Application in their subscription. ## Prerequisites In order for the following steps to work you must first have [published a Moodle Based Managed Application](PublishMoodleManagedApplication.md) into your service catalog. ## Consume the Managed Application Once the Moodle on Azure Managed Application is published to your service catalog you can now depoloy it from within the portal or using the CLI. In the following commands we'll see how to do this in the CLI. ### Setup a Resource Group for the Application First we need to get the id of the application. This was returned in the output of the command to create the service catalog entry. However, we'll use the CLI to retrieve it and record it into a variable: ``` bash MOODLE_MANAGED_APP_ID=$(az managedapp definition show --name $MOODLE_MANAGED_APP_NAME --resource-group $MOODLE_SERVICE_CATALOG_RG_NAME --query id --output tsv) ``` Create the application resource group, this is the group in which the customer will see the managed application. ``` bash az group create --name $MOODLE_DEPLOYMENT_RG_NAME --location=$MOODLE_DEPLOYMENT_LOCATION ``` Results: ``` json { "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/MoodleManagedApp", "location": "southcentralus", "managedBy": null, "name": "MoodleManagedApp", "properties": { "provisioningState": "Succeeded" }, "tags": null } ``` ### Customer Deployment When a customer wants to deploy an application they can do so using either the Portal or the CLI. In this section we'll look at how this is done in the CLI. #### Providing Parameters If we were using the portal our `CreateUIDefinition.json` file would be used to create a user interface to define the parameters needed in `mainTemplate.json`. When using the CLI we need to provide parameter values for any parameters that don't have a default. To make it easier to manage we'll put these parameter values into environment variables. For convenience our `mainTemplate.json` file has defaults for all values. This means that there is no need to provide parameters in the commandline, though you can override the defaults if you want to by adding the `--parameters` attribute. This attribute can take either a JSON string or a filename (preceded with an '@', e.g. '--parameters @parameters.json`) containing a JSON definition for the paramters, e.g. { "parameterName": { "value": "some value" }, "anotherParameterName": { "value": "another value" } } The Moodle template provides sensible defaults for almost every parameter, the one exception to this is the SSH Public Key, used to provide secure access to the VMs. For this example we will use the defaults for all parameters, but we still need to create a parameters file. A template file is provided here (see `parameters-template.json`). The following command will replace the placeholder in the parameters template file with an SSH key used for testing puporses (this is created as part of the envrionment setup in the prerequisites): ``` bash ssh_pub_key=`cat $MOODLE_SSH_KEY_FILENAME.pub` echo $ssh_pub_key sed "s|GEN-SSH-PUB-KEY|$ssh_pub_key|g" parameters-template.json > $MOODLE_MANAGED_APP_WORKSPACE/$MOODLE_DEPLOYMENT_NAME/parameters.json ``` If you want to have more control over the deployment configuration simply add parameters to the template file and use that to create parameter files for specific deployments. ### Deploying the application Deploy the managed application and corresponding infrastructure. ``` bash az managedapp create --name $MOODLE_DEPLOYMENT_NAME --location $MOODLE_DEPLOYMENT_LOCATION --kind ServiceCatalog --resource-group $MOODLE_DEPLOYMENT_RG_NAME --managedapp-definition-id $MOODLE_MANAGED_APP_ID --managed-rg-id $MOODLE_MANAGED_RG_ID --parameters @$MOODLE_MANAGED_APP_WORKSPACE/$MOODLE_DEPLOYMENT_NAME/parameters.json ``` ================================================ FILE: managedApplication/Environment.md ================================================ # Setup Environment For convenience most of the configuration values we need to create and manage our Moodle Managed Application we'll create a numer of Environment Variables. In order to store any generated files and configurations we will also create a workspace. NOTE: If you are running these scripts through SimDem you can customize these values by copying and editing `env.json` into `env.local.json`. ## Setup for Publishing the Moodle Managed Application ``` bash MOODLE_MANAGED_APP_OWNER_GROUP_NAME=MoodleOwner MOODLE_MANAGED_APP_OWNER_NICKNAME=MoodleOwner MOODLE_SERVICE_CATALOG_LOCATION=southcentralus MOODLE_SERVICE_CATALOG_RG_NAME=MoodleManagedAppServiceCatalogRG MOODLE_MANAGED_APP_NAME=MoodleManagedApp MOODLE_MANAGED_APP_LOCK_LEVEL=ReadOnly MOODLE_MANAGED_APP_DISPLAY_NAME=Moodle MOODLE_MANAGED_APP_DESCRIPTION="Moodle on Azure as a Managed Application" ``` ## Setup for Consuming the Moodle Managed Application Create an id for the resource group that will be managed by the managed application provider. This is the resource group that infrastructure will be deployed into. The end user does not, generally, manage this group. ``` bash SUBSCRIPTION_ID=$(az account show --query id --output tsv) MOODLE_MANAGED_RG_ID=/subscriptions/$SUBSCRIPTION_ID/resourceGroups/MoodleInfrastructure ``` We'll also need a resource group for the application deployment. This is the resource group into which the application is deployed. This is the resource group that the provider of the managed application will have access to. ``` bash MOODLE_DEPLOYMENT_RG_NAME=MoodleManagedAppRG MOODLE_DEPLOYMENT_LOCATION=southcentralus MOODLE_DEPLOYMENT_NAME=MoodleManagedApp ``` ## Workspace We need a workspace for storing configuration files and other per-deployment artifacts: ``` shell MOODLE_MANAGED_APP_WORKSPACE=~/.moodle mkdir -p $MOODLE_MANAGED_APP_WORKSPACE/$MOODLE_DEPLOYMENT_NAME ``` ## SSH Key We use SSH for secure communication with our hosts. The following line will check there is a valid SSH key available and, if not, create one. ``` MOODLE_SSH_KEY_FILENAME=~/.ssh/moodle_managedapp_id_rsa if [ ! -f "$MOODLE_SSH_KEY_FILENAME" ]; then ssh-keygen -t rsa -N "" -f $MOODLE_SSH_KEY_FILENAME; fi ``` ================================================ FILE: managedApplication/PublishMoodleManagedApplication.md ================================================ # Publish a Moodle Based Managed Appliction to Service Catalog In this document we will look at how to publish a Moodle based Managed Application into your Service Catalog so that you can allow your customers to deploy the application into their subscriptions. If you are not sure why you would do this you might want to read our [Moodle Based Managed Application Introduction](README.md) first. ## Prerequisites In the following sections we demonstrate how to use the Azure CLI to work with a Moodle based Managed Application. For convenience these commands use a variety of [environment variables](Environment.md) that should be configured first. ## Defining the Resources (mainTemplate.json) The `mainTemplate.json` file defines the Azure resources that are provisioned as part of the managed application. We've already done the majority of the work here for you (see `azuredeploy.json` in the root of this repository). The `mainTemplate.json` file is where you customize the configuration and, optionally, add additional resources. For the purposes of our demo we will use the ARM template from the root of our project as the main tamplate. This file is a regular [Azure Resource Manager template](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-overview). ## User Interface Definition (createUIDefinition.json) The `createUIDefinition.json` file describes the user interface needed to configure the managed application. It defines how the user provides input for each of the parameters (specified in `mainTemplate.json`). An initial `createUIDefinition.json` file is provided in `managedApplication/creatueUIDefinition.json`. This files is sufficient to get you started building your own Moodle based Managed Applications. See [Create UI Definition documentation](https://docs.microsoft.com/en-us/azure/managed-applications/create-uidefinition-overview) for more information. ## Create an Microsoft Entra ID User Group or Application You will need to create one ore more user group or appliction in Microsoft Entra ID to allow you to manage the applications resources on behalf of your customer. These groups or application can be given any built-in Role-Based Access Control (RBAC) role, such as 'Owner' or 'Contributor'. By creating more than one such group or application you can configure access to your customers resources based on the specific needs of each role in your organization. Azure has full documentation on [creating a group in Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/fundamentals/how-to-manage-groups). The commands below will create a single 'owner' role for use in the examples below. If the Group already exists we don't want to create a new one, so we will try to get the Group ID first: ``` bash MOODLE_MANAGED_APP_AD_ID=$(az ad group list --filter="displayName eq '$MOODLE_MANAGED_APP_OWNER_GROUP_NAME'" --query [0].objectId --output tsv) ``` At this point MOODLE_MANAGED_APP_AD_ID will either be empty or it will have the ID of an existing group. If it is empty we need to create the group and grab its ID: ``` bash if [ -z "$MOODLE_MANAGED_APP_AD_ID" ]; then az ad group create --display-name $MOODLE_MANAGED_APP_OWNER_GROUP_NAME --mail-nickname=$MOODLE_MANAGED_APP_OWNER_NICKNAME; fi ``` Let's ensure that we have the object ID even if we created a new one. ``` bash MOODLE_MANAGED_APP_AD_ID=$(az ad group list --filter="displayName eq '$MOODLE_MANAGED_APP_OWNER_GROUP_NAME'" --query [0].objectId --output tsv) ``` You will also need the Role ID for your chosen role, here we will use the built-in 'Owner' role: ``` bash MOODLE_MANAGED_APP_ROLE_ID=$(az role definition list --name Owner --query [].name --output tsv) ``` The Azure documentation has more information on how to work with [Microsoft Entra ID](https://learn.microsoft.com/en-us/azure/role-based-access-control/). ## Create a Resource Group for the Managed Application Service Catalog Entry ``` bash az group create --name $MOODLE_SERVICE_CATALOG_RG_NAME --location $MOODLE_SERVICE_CATALOG_LOCATION ``` ## Publish to your Service Catalog using Azure CLI You can publish a Managed Application definition into your Service Catalog using the Azure CLI. For convenience we'll set a few environment variables to make it easier to work with the application. We'll need to construct the authorization configuration from the app and role IDs retrieved earlier. ``` bash MOODLE_MANAGED_APP_AUTHORIZATIONS=$MOODLE_MANAGED_APP_AD_ID:$MOODLE_MANAGED_APP_ROLE_ID ``` The following command will add your managed application definition to the Service Catalog. ``` bash az managedapp definition create --name $MOODLE_MANAGED_APP_NAME --location $MOODLE_SERVICE_CATALOG_LOCATION --resource-group $MOODLE_SERVICE_CATALOG_RG_NAME --lock-level $MOODLE_MANAGED_APP_LOCK_LEVEL --display-name $MOODLE_MANAGED_APP_DISPLAY_NAME --description "$MOODLE_MANAGED_APP_DESCRIPTION" --authorizations="$MOODLE_MANAGED_APP_AUTHORIZATIONS" --main-template=@../azuredeploy.json --create-ui-definition=@createUIDefinition.json ``` Results: ``` json { "artifacts": [ { "name": "ApplicationResourceTemplate", "type": "Template", "uri": "https://prdsapplianceprodsn01.blob.core.windows.net/applicationdefinitions/84205_325E7C3499FB4190AA871DF746C67705_8D748DA35A5166F6BF319C41398E89D9953014D8/applicationResourceTemplate.json?sv=2014-02-14&sr=b&sig=PyYyl6dzf0vVyrde2yJZ73h6h9fqbXHwMJuXf0lGFr8%3D&se=2118-03-15T21:33:33Z&sp=r" }, { "name": "CreateUiDefinition", "type": "Custom", "uri": "https://management.azure.com/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/MoodleManagedAppServiceCatalogRG/providers/Microsoft.Solutions/applicationDefinitions/MoodleManagedApp/applicationArtifacts/CreateUiDefinition?api-version=2017-09-01" } ], "authorizations": [ { "principalId": "fdc3f6fb-cc24-4182-9943-b63e0ed67285", "roleDefinitionId": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" } ], "createUiDefinition": null, "description": "Moodle on Azure as a Managed Application", "displayName": "Moodle", "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/MoodleManagedAppServiceCatalogRG/providers/Microsoft.Solutions/applicationDefinitions/MoodleManagedApp", "identity": null, "isEnabled": "True", "location": "southcentralus", "lockLevel": "ReadOnly", "mainTemplate": null, "managedBy": null, "name": "MoodleManagedApp", "packageFileUri": null, "resourceGroup": "MoodleManagedAppServiceCatalogRG", "sku": null, "tags": null, "type": "Microsoft.Solutions/applicationDefinitions" } ``` ### [OPTIONAL] Package the files The `mainTemplate.json` and `createUIDefinition.json` files can be packaged together in a zip file. Both files should be at the root level of the zip. Once created the package needs to be uploaded to a location accessible to Azure. We've published the samples to GitHub so you can experiment with minimal effort. To use a package file remove the `--create-ui-definition` and `--main-tamplate` arguments from the above CLI command instead provide a URI for the package using `--package-file-uri` argument. ## Next Steps Now that you have published a Moodle based Managed Application on Azure you can: 1. [Deploy Moodle into Customer Subscription](DeployMoodleManagedApp.md) ================================================ FILE: managedApplication/README.md ================================================ # Azure Managed Application [Azure Managed Applications](https://docs.microsoft.com/en-us/azure/managed-applications/overview) enable you to offer your Moodle based solutions to customers via the [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/) or a Service Catalog. You define the infrastructure for the solution, using the ARM templates in this repository as a starting point, along with the terms for ongoing management of the solution. The billing for your solution is handled through Azure billing. ## Why the Azure Marketplace and Azure Managed Applications for Moodle Hosting Providers The Azure Marketplace allows you the capability of offering an Azure-certified Moodle solution via a modern marketplace. When a customer runs Moodle from the Azure Marketplace they have the confidence that the Moodle solution certified and optimized to run on Azure, and that they can get support should they need it. Until recently it was difficult for many Moodle hosting providers to offer Moodle via the Azure Marketplace, in particular because after a marketplace solution was deployed, customers would still be responsible for maintaining, updating, or servicing their environment. As customers are not always experts on cloud infrastructure this made offering a Marketplace offering with a Moodle-hoster backed SLA difficult. Moreover, a customer had full-access to the resources (i.e. VMs, databases, etc.) in the solution once deployed, meaning they could easily make a change to the underlying infrastructure (such as accidentally deleting a critical VM) that might have rendered the solution unusable. With the advent of Azure Managed Application for the Azure Marketplace, the Moodle Hosting provider can now specify exactly which underlying infrastructure resources for a Moodle solution a customer does (and does not) have access to. This means that a Moodle hoster can now prevent a customer from make a change which could take down your Moodle solution and render your SLA void. Moreover, although customers continue to deploy your Moodle solution offering in their subscriptions just like all Azure Marketplace offerings, the customer does not have to maintain, update, or service them and troubleshooting and diagnosing of issues can be done by the Moodle hoster on-behalf of the customer. ## Why Moodle Managed Applications for IT Teams? For IT teams, managed applications enable you to offer pre-approved configuration of Moodle to users in the organization. For example, if to be compliant with organizational standards you require users deploy Moodle with certain version number, database SKUs or networking/security configurations, you can enforce compliance. Read more about [Managed Applications](https://docs.microsoft.com/en-us/azure/managed-applications/overview), or keep reading here to see how to quickly get started providing your own Moodle based services as Managed Applications. ## Next Steps 1. [Publish a Managed Application Definition](PublishMoodleManagedApplication.md) 2. [Deploy a Moodle Based Managed Application](DeployMoodleManagedApp.md) 3. [Learn about submitting your application to the Azure Marketplace](https://docs.microsoft.com/en-us/azure/marketplace/marketplace-publishers-guide) 4. [Submit your application to the Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/sell/nominate) ================================================ FILE: managedApplication/createServiceCatlogUpdate.sh ================================================ # This script will create a Managed Application from the Azure/Moodle ARM template # see https://github.com/Azure/Moodle/tree/master/managedApplication # Application Configuration export VERSION_NUMBER=1 export MOODLE_MANAGED_APP_DISPLAY_NAME=MoodleManagedApp export MOODLE_MANAGED_APP_NAME=MoodleManagedApp_$(whoami)_$VERSION_NUMBER export MOODLE_MANAGED_APP_DESCRIPTION="Testing the Moodle ARM template as a managed application." export MOODLE_MANAGED_APP_OWNER_GROUP_NAME=$MOODLE_MANAGED_APP_NAME export MOODLE_MANAGED_APP_OWNER_NICKNAME=$MOODLE_MANAGED_APP_NAME export MOODLE_SERVICE_CATALOG_RG_NAME=Catalog_RG_$MOODLE_MANAGED_APP_NAME export MOODLE_MANAGED_APP_LOCK_LEVEL=None export MOODLE_SERVICE_CATALOG_LOCATION=WestUS export PATH_TO_ARM_TEMPLATE=../azuredeploy.json export PATH_TO_MOODLE_CREATEUI_DEF=createUIDefinition.json # Publish A Managed Application To Service Catalog # AD Config echo "Configuring AD" echo "Getting Application AD ID for $MOODLE_MANAGED_APP_OWNER_GROUP_NAME" MOODLE_MANAGED_APP_AD_ID=$(az ad group list --display-name=$MOODLE_MANAGED_APP_OWNER_GROUP_NAME --query [0].objectId --output tsv) # The following line should create a new group, if necessary, but it fails with insufficient permissions # if [ -z "$MOODLE_MANAGED_APP_AD_ID" ]; then az ad group create --display-name $MOODLE_MANAGED_APP_OWNER_GROUP_NAME --mail-nickname=$MOODLE_MANAGED_APP_OWNER_NICKNAME; fi # Not sure how to fix it so, for now tell user to create in portal, which works fine if [ -z "$MOODLE_MANAGED_APP_AD_ID" ] then echo "AD group doesn't exist.\n" echo "There's a bug in the script which prevents this being automated (see comments, should be fixable by someone who knows)\n" echo "For now, you need to create an ad group with the name $MOODLE_MANAGED_APP_OWNER_GROUP_NAME and owner $MOODLE_MANAGED_APP_OWNER_NICKNAME see https://ms.portal.azure.com/#blade/Microsoft_AAD_IAM/GroupsManagementMenuBlade/AllGroups" read -p "Press any key when done... " -n1 -s; echo "Continuing..." MOODLE_MANAGED_APP_AD_ID=$(az ad group list --display-name=$MOODLE_MANAGED_APP_OWNER_GROUP_NAME --query [0].objectId --output tsv) fi if [ -z "$MOODLE_MANAGED_APP_AD_ID"] then >&2 echo "Failed to get a Managed App AD ID. If you just created this it may be that it is still propogating. Rerun the script." exit 1 else echo "Managed App AD ID is $MOODLE_MANAGED_APP_AD_ID" fi MOODLE_MANAGED_APP_ROLE_ID=$(az role definition list --name Owner --query [].name --output tsv) echo "Managed App Role ID is $MOODLE_MANAGED_APP_ROLE_ID" # Create a Resource Group echo "Creating the resource group for the service catalog using the name $MOODLE_SERVICE_CATALOG_RG_NAME and location $MOODLE_SERVICE_CATALOG_LOCATION" az group create --name $MOODLE_SERVICE_CATALOG_RG_NAME --location $MOODLE_SERVICE_CATALOG_LOCATION # Publish to the Service Catalog echo "Publishing the application to the service catalog using the name $MOODLE_MANAGED_APP_NAME" MOODLE_MANAGED_APP_AUTHORIZATIONS=$MOODLE_MANAGED_APP_AD_ID:$MOODLE_MANAGED_APP_ROLE_ID az managedapp definition create \ --name $MOODLE_MANAGED_APP_NAME \ --location $MOODLE_SERVICE_CATALOG_LOCATION \ --resource-group $MOODLE_SERVICE_CATALOG_RG_NAME \ --lock-level $MOODLE_MANAGED_APP_LOCK_LEVEL \ --display-name $MOODLE_MANAGED_APP_DISPLAY_NAME \ --description "$MOODLE_MANAGED_APP_DESCRIPTION" \ --authorizations="$MOODLE_MANAGED_APP_AUTHORIZATIONS" \ --main-template=@$PATH_TO_ARM_TEMPLATE \ --create-ui-definition=@$PATH_TO_MOODLE_CREATEUI_DEF MOODLE_MANAGED_APP_ID=$(az managedapp definition show --name $MOODLE_MANAGED_APP_NAME --resource-group $MOODLE_SERVICE_CATALOG_RG_NAME --query id --output tsv) echo echo "###############################################################" echo "Assuming no errors reporteed above, you can now deploy an application in the portal at https://ms.portal.azure.com/#blade/HubsExtension/BrowseResource/resourceType/Microsoft.Solutions%2FapplicationDefinitions" echo "###############################################################" ================================================ FILE: managedApplication/createUIDefinition.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", "handler": "Microsoft.Compute.MultiVm", "version": "0.1.2-preview", "parameters": { "basics": [ {} ], "steps": [ { "name": "credentialsConfig", "label": "VM Credentials", "subLabel": { "preValidation": "Provide credentials for accessing the VMs in your application.", "postValidation": "Credentials configured." }, "bladeTitle": "Credentials", "elements": [ { "name": "sshPublicKey", "type": "Microsoft.Common.TextBox", "label": "SSH Public Key", "toolTip": "Public SSH Key to use for access", "constraints": { "required": true } } ] } ], "outputs": { "sshPublicKey": "[steps('credentialsConfig').sshPublicKey]" } } } ================================================ FILE: managedApplication/parameters-template.json ================================================ { "sshPublicKey": { "value": "GEN-SSH-PUB-KEY" }, "redisDeploySwitch": { "value": false }, "dbServerType": { "value": "mysql" }, "fileServerType": { "value": "nfs" }, "autoscaleVmSku": { "value": "Standard_DS1_v2" }, "fileServerDiskCount": { "value": 2 } } ================================================ FILE: metadata.json ================================================ { "itemDisplayName": "Autoscalable Moodle on Azure", "description": "Deploys an autoscaling Moodle cluster with configurable objectfs storage, Azure redis, Azure MySQL/Postgres/MSSQu, and Elasticsearch. Can be configured for very small or very large sites. Deploys frontend components to a private network with a jumphost to access nodes. Requires keyed SSH access.", "summary": "Moodle autoscale with redis/db/elasticsearch", "githubUsername": "hosungsmsft", "dateUpdated": "2018-04-20" } ================================================ FILE: migration/azure-fileshare-sa-deploy.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "storageAccountType": { "defaultValue": "Premium_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type." }, "type": "string" }, "fileServerDiskSize": { "defaultValue": "1024", "metadata": { "description": "Size of the azure file share in GB." }, "type": "string" } }, "resources": [ { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "storageAccountTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[variables('moodleCommon')]" } }, "templateLink": { "uri": "[concat(variables('moodleCommon').baseTemplateUrl,'storageAccount.json',variables('moodleCommon').artifactsSasToken)]" } } }, { "type": "Microsoft.Storage/storageAccounts/fileServices/shares", "apiVersion": "2019-06-01", "name": "[concat(variables('storageName'), '/default/moodle')]", "dependsOn": [ "Microsoft.Resources/deployments/storageAccountTemplate" ], "properties": { "accessTier": "[if(equals(variables('moodleCommon').storageAccountType, 'Premium_LRS'), 'Premium', 'TransactionOptimized')]", "shareQuota": "[int(variables('moodleCommon').fileServerDiskSize)]", "enabledProtocols": "SMB" } } ], "outputs": { "storageAccountName": { "type": "string", "value": "[variables('moodleCommon').storageAccountName]" }, "storageAccountType": { "type": "string", "value": "[variables('moodleCommon').storageAccountType]" }, "fileServerDiskSize": { "type": "string", "value": "[variables('moodleCommon').fileServerDiskSize]" } }, "variables": { "_artifactsLocation": "https://raw.githubusercontent.com/Azure/Moodle/master/", "_artifactsLocationSasToken": "", "unsupportedLocations": [ "eastus2euap", "westcentralus" ], "documentationLine1": "Some of the Azure Services used by moodle migration are not available in few regions. Those regions are declared above.", "documentationLine2": "If resource group belong to one of those unsupported regions, then use default region 'westus' for deployment.", "rgLocation": "[toLower(resourceGroup().location)]", "location": "[if(contains(variables('unsupportedLocations'), variables('rgLocation')), 'westus', variables('rgLocation'))]", "moodleCommon": { "baseTemplateUrl": "[concat(variables('_artifactsLocation'), 'nested/')]", "artifactsSasToken": "[variables('_artifactsLocationSasToken')]", "fileServerType": "azurefiles", "location": "[variables('location')]", "storageAccountName": "[tolower(concat('abs',variables('resourceprefix')))]", "storageAccountType": "[parameters('storageAccountType')]", "fileServerDiskSize": "[parameters('fileServerDiskSize')]" }, "resourceprefix": "[substring(uniqueString(resourceGroup().id, 'mainTemplate'), 3, 6)]", "storageName": "[concat(variables('moodleCommon').storageAccountName,if(equals(variables('moodleCommon').fileServerType, 'azurefiles'), 'af', 'naf'))]" } } ================================================ FILE: migration/azuredeploy-migration.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "redisDeploySwitch": { "defaultValue": "false", "metadata": { "description": "Switch to deploy a redis cache or not. Note that certain versions of Moodle (e.g., 3.1) don't work well with Redis, so use this only for known well-working Moodle versions (e.g., 3.4)." }, "allowedValues": [ "true", "false" ], "type": "string" }, "httpsTermination": { "allowedValues": [ "VMSS", "AppGw", "None" ], "defaultValue": "VMSS", "metadata": { "description": "Indicates where https termination occurs. 'VMSS' is for https termination at the VMSS instance VMs (using nginx https proxy). 'AppGw' is for https termination with an Azure Application Gateway. When selecting this, you need to specify all appGw* parameters. 'None' is for testing only with no https. 'None' may not be used with a separately configured https termination layer. If you want to use the 'None' option with your separately configured https termination layer, you'll need to update your Moodle config.php manually for $cfg->wwwroot and $cfg->sslproxy." }, "type": "string" }, "loadBalancerSku": { "defaultValue": "Standard", "allowedValues": [ "Basic", "Standard" ], "metadata": { "description": "Loadbalancer SKU" }, "type": "string" }, "moodleVersion": { "allowedValues": [ "MOODLE_39_STABLE", "MOODLE_38_STABLE" ], "defaultValue": "MOODLE_38_STABLE", "metadata": { "description": "The Moodle version you want to install." }, "type": "string" }, "sshPublicKey": { "metadata": { "description": "ssh public key" }, "type": "string" }, "webServerType": { "defaultValue": "nginx", "allowedValues": [ "apache", "nginx" ], "metadata": { "description": "Web server type" }, "type": "string" }, "autoscaleVmSku": { "defaultValue": "Standard_DS2_v2", "metadata": { "description": "VM size for autoscaled web VMs" }, "type": "string" }, "autoscaleVmCountMax": { "defaultValue": "10", "metadata": { "description": "Maximum number of autoscaled web VMs" }, "type": "string" }, "phpVersion": { "allowedValues": [ "7.2", "7.3", "7.4" ], "defaultValue": "7.4", "metadata": { "description": "php version" }, "type": "string" }, "dbServerType": { "defaultValue": "mysql", "allowedValues": [ "postgres", "mysql" ], "metadata": { "description": "Database type" }, "type": "string" }, "moodleDbName": { "defaultValue": "moodle", "metadata": { "description": "Moodle Database name" }, "type": "string" }, "moodleDbUser": { "defaultValue": "moodle", "metadata": { "description": "Moodle Database username. This user is different from Database admin user." }, "type": "string" }, "mysqlPgresVcores": { "allowedValues": [ "1", "2", "4", "8", "16", "32" ], "defaultValue": "2", "metadata": { "description": "MySql/Postgresql vCores. For Basic tier, only 1 & 2 are allowed. For GeneralPurpose tier, 2, 4, 8, 16, 32 are allowed. For MemoryOptimized, 2, 4, 8, 16 are allowed." }, "type": "string" }, "mysqlPgresStgSizeGB": { "defaultValue": "125", "metadata": { "description": "MySql/Postgresql storage size in GB. Minimum 5GB, increase by 1GB, up to 1TB (1024 GB)" }, "type": "string" }, "mysqlPgresSkuTier": { "allowedValues": [ "Basic", "GeneralPurpose", "MemoryOptimized" ], "defaultValue": "GeneralPurpose", "metadata": { "description": "MySql/Postgresql sku tier" }, "type": "string" }, "mysqlPgresSkuHwFamily": { "allowedValues": [ "Gen4", "Gen5" ], "defaultValue": "Gen5", "metadata": { "description": "MySql/Postgresql sku hardware family. Central US is Gen4 only, so make sure to change this parameter to Gen4 if your deployment is on Central US." }, "type": "string" }, "mysqlVersion": { "allowedValues": [ "5.6", "5.7" ], "defaultValue": "5.7", "metadata": { "description": "Mysql version" }, "type": "string" }, "fileServerDiskSize": { "defaultValue": "1024", "metadata": { "description": "Size per disk for gluster nodes or nfs server" }, "type": "string" }, "vNetAddressSpace": { "defaultValue": "172.31.0.0", "metadata": { "description": "Address range for the Moodle virtual network and various subnets - presumed /16 for a newly created vnet in case customVnetId is blank. Further subneting (a number of */24 subnets starting from the xxx.yyy.zzz.0/24 will be created on a newly created vnet or your BYO-vnet (specified in customVnetId parameter)." }, "type": "string" }, "ubuntuVersion": { "type": "string", "allowedValues": [ "18.04-LTS" ], "defaultValue": "18.04-LTS" } }, "variables": { "_artifactsLocation": "https://raw.githubusercontent.com/Azure/Moodle/master/", "_artifactsLocationSasToken": "", "unsupportedLocations": [ "eastus2euap", "westcentralus" ], "documentationLine1": "Some of the Azure Services used by moodle migration are not available in few regions. Those regions are declared above.", "documentationLine2": "If resource group belong to one of those unsupported regions, then use default region 'westus' for deployment.", "rgLocation": "[toLower(resourceGroup().location)]", "location": "[if(contains(variables('unsupportedLocations'), variables('rgLocation')), 'westus', variables('rgLocation'))]" }, "resources": [ { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "mainTemplate", "properties": { "mode": "Incremental", "parameters": { "_artifactsLocation": { "value": "[variables('_artifactsLocation')]" }, "_artifactsLocationSasToken": { "value": "[variables('_artifactsLocationSasToken')]" }, "redisDeploySwitch": { "value": "[bool(parameters('redisDeploySwitch'))]" }, "httpsTermination": { "value": "[parameters('httpsTermination')]"}, "loadBalancerSku": { "value": "[parameters('loadBalancerSku')]"}, "siteURL": { "value": ""}, "moodleVersion": { "value": "[parameters('moodleVersion')]"}, "sshPublicKey": { "value": "[parameters('sshPublicKey')]" }, "controllerVmSku": { "value": "Standard_DS1_v2"}, "webServerType": { "value": "[parameters('webServerType')]"}, "autoscaleVmSku": { "value": "[parameters('autoscaleVmSku')]" }, "autoscaleVmCountMax": { "value": "[int(parameters('autoscaleVmCountMax'))]" }, "phpVersion": { "value": "[parameters('phpVersion')]" }, "dbServerType": { "value": "[parameters('dbServerType')]" }, "dbLogin": { "value": "dbadmin" }, "moodleDbName": { "value": "[parameters('moodleDbName')]" }, "moodleDbUser": { "value": "[parameters('moodleDbUser')]" }, "mysqlPgresVcores": { "value": "[int(parameters('mysqlPgresVcores'))]" }, "mysqlPgresStgSizeGB": { "value": "[int(parameters('mysqlPgresStgSizeGB'))]" }, "mysqlPgresSkuTier": { "value": "[parameters('mysqlPgresSkuTier')]" }, "mysqlPgresSkuHwFamily": { "value": "[parameters('mysqlPgresSkuHwFamily')]" }, "mysqlVersion": { "value": "[parameters('mysqlVersion')]" }, "postgresVersion": { "value": "9.6" }, "fileServerType": { "value": "azurefiles" }, "storageAccountType": { "value": "Premium_LRS" }, "fileServerDiskSize": { "value": "[int(parameters('fileServerDiskSize'))]" }, "searchType": { "value": "none" }, "azureSearchSku": { "value": "basic" }, "vNetAddressSpace": { "value": "[parameters('vNetAddressSpace')]"}, "ubuntuVersion": { "value": "[parameters('ubuntuVersion')]"}, "location": { "value": "[variables('location')]" }, "enableAccelNwForOtherVmsSwitch": { "value" : false }, "isMigration": { "value": true } }, "templateLink": { "uri": "[concat(variables('_artifactsLocation'), 'azuredeploy.json', variables('_artifactsLocationSasToken'))]" } } } ], "outputs": { "mainTemplateOutputs": { "type": "object", "value": "[reference('mainTemplate').outputs]" } } } ================================================ FILE: nested/appgw.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "subnetIdAppGw": { "metadata": { "description": "Azure resource ID of the subnet where this App Gw should be deployed" }, "type": "string" }, "sslCertData": { "metadata": { "description": "Base64-encoded PFX (no password protected) file content for the SSL cert to be used on the App Gateway for SSL termination. Should be passed from an Azure Key Vault." }, "type": "securestring" } }, "resources": [ { "type": "Microsoft.Network/applicationGateways", "name": "[parameters('moodleCommon').appGwName]", "location": "[parameters('moodleCommon').location]", "apiVersion": "2019-11-01", "properties": { "sku": { "name": "[parameters('moodleCommon').appGwSkuName]", "tier": "[parameters('moodleCommon').appGwSkuTier]", "capacity": "[parameters('moodleCommon').appGwSkuCapacity]" }, "gatewayIPConfigurations": [ { "name": "appGwIpConfig", "properties": { "subnet": { "id": "[parameters('subnetIdAppGw')]" } } } ], "frontendIPConfigurations": [ { "name": "appGwFrontendIP", "properties": { "publicIPAddress": { "id": "[variables('appGwPublicIPAddressID')]" } } } ], "frontendPorts": [ { "name": "httpsFrontendPort", "properties": { "port": 443 } }, { "name": "httpFrontendPort", "properties": { "port": 80 } } ], "backendAddressPools": [ { "name": "[variables('appGwBePoolName')]" } ], "backendHttpSettingsCollection": [ { "name": "appGwBackendHttpSettings", "properties": { "port": 80, "protocol": "Http", "cookieBasedAffinity": "Disabled" } } ], "sslCertificates": [ { "name": "appGatewaySslCert", "properties": { "data": "[parameters('sslCertData')]" } } ], "httpListeners": [ { "name": "appGwHttpsListener", "properties": { "frontendIPConfiguration": { "Id": "[concat(variables('appGwID'), '/frontendIPConfigurations/appGwFrontendIP')]" }, "frontendPort": { "Id": "[concat(variables('appGwID'), '/frontendPorts/httpsFrontendPort')]" }, "protocol": "Https", "sslCertificate": { "Id": "[concat(variables('appGwID'), '/sslCertificates/appGatewaySslCert')]" } } }, { "name": "appGwHttpListener", "properties": { "frontendIPConfiguration": { "Id": "[concat(variables('appGwID'), '/frontendIPConfigurations/appGwFrontendIP')]" }, "frontendPort": { "Id": "[concat(variables('appGwID'), '/frontendPorts/httpFrontendPort')]" }, "protocol": "Http" } } ], "redirectConfigurations": [ { "name": "httpToHttps", "properties": { "redirectType":"Permanent", "includePath" : true, "includeQueryString" : true, "targetListener": { "id": "[concat(variables('appGwID'), '/httpListeners/appGwHttpsListener')]" } } } ], "requestRoutingRules": [ { "Name": "httpsRule", "properties": { "ruleType": "Basic", "httpListener": { "id": "[concat(variables('appGwID'), '/httpListeners/appGwHttpsListener')]" }, "backendAddressPool": { "id": "[concat(variables('appGwID'), '/backendAddressPools/', variables('appGwBePoolName'))]" }, "backendHttpSettings": { "id": "[concat(variables('appGwID'), '/backendHttpSettingsCollection/appGwBackendHttpSettings')]" } } }, { "Name": "httpRedirectRule", "properties": { "ruleType": "Basic", "httpListener": { "id": "[concat(variables('appGwID'), '/httpListeners/appGwHttpListener')]" }, "redirectConfiguration": { "id": "[concat(variables('appGwID'), '/redirectConfigurations/httpToHttps')]" } } } ] } } ], "variables": { "documentation1": "This sub-template creates an Azure Application Gateway for SSL offloading. It expects certain values in the 'common' datastructure.", "appGwBePoolName": "[parameters('moodleCommon').appGwBePoolName]", "appGwPublicIPAddressID": "[resourceId('Microsoft.Network/publicIPAddresses',parameters('moodleCommon').appGwPipName)]", "appGwID": "[resourceId('Microsoft.Network/applicationGateways', parameters('moodleCommon').appGwName)]" } } ================================================ FILE: nested/controller.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "subnetIdWeb": { "metadata": { "description": "Azure resource ID of the subnet where this VM is to be deployed" }, "type": "string" }, "ctlrPubIpId": { "metadata": { "description": "Resource ID of the controller VM public IP address" }, "type": "string" }, "vmSetupParamsObj": { "metadata": { "description": "JSON-structured VM setup params that'll be injected to the VM (through cloud-init) and used by the custom script (install_moodle.sh)" }, "type": "object" } }, "resources": [ { "type": "Microsoft.Network/networkSecurityGroups", "apiVersion": "2017-10-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').ctlrNsgName]", "properties": { "securityRules": [ { "name": "Allow_SSH", "properties": { "access": "Allow", "destinationAddressPrefix": "*", "destinationPortRange": "22", "direction": "Inbound", "priority": 1000, "protocol": "Tcp", "sourceAddressPrefix": "*", "sourcePortRange": "*" } }, { "name": "Allow_http", "properties": { "access": "Allow", "destinationAddressPrefix": "*", "destinationPortRange": "80", "direction": "Inbound", "priority": 1005, "protocol": "Tcp", "sourceAddressPrefix": "*", "sourcePortRange": "*" } } ] }, "tags": { "displayName": "Controller NSG" } }, { "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2017-10-01", "dependsOn": [ "[concat('Microsoft.Network/networkSecurityGroups/', parameters('moodleCommon').ctlrNsgName)]" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').ctlrNicName]", "properties": { "networkSecurityGroup": { "id": "[variables('nsgRef')]" }, "ipConfigurations": [ { "name": "ipcfgctlr", "properties": { "privateIPAllocationMethod": "Dynamic", "publicIPAddress": { "id": "[parameters('ctlrPubIpId')]" }, "subnet": { "id": "[parameters('subnetIdWeb')]" } } } ], "enableAcceleratedNetworking": "[parameters('moodleCommon').enableAccelNwForCtlrVmSwitch]" }, "tags": { "displayName": "ctlrNic" } }, { "type": "Microsoft.Compute/virtualMachines", "apiVersion": "2017-03-30", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', parameters('moodleCommon').ctlrNicName)]" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').ctlrVmName]", "properties": { "hardwareProfile": { "vmSize": "[parameters('moodleCommon').controllerVmSku]" }, "networkProfile": { "networkInterfaces": [ { "id": "[variables('nicRef')]" } ] }, "osProfile": { "adminUsername": "[parameters('moodleCommon').sshUsername]", "computerName": "[parameters('moodleCommon').ctlrVmName]", "secrets": "[parameters('moodleCommon').ctlrVmSecrets]", "customData": "[base64(concat('#cloud-config\nwrite_files:\n- encoding: b64\n content: ', base64(string(parameters('vmSetupParamsObj'))), '\n owner: root:root\n path: ', parameters('moodleCommon').moodleOnAzureConfigsJsonPath, '\n permissions: ', variables('singleQuote'), '0400', variables('singleQuote')))]", "linuxConfiguration": { "disablePasswordAuthentication": true, "ssh": { "publicKeys": [ { "path": "[concat('/home/', parameters('moodleCommon').sshUsername, '/.ssh/authorized_keys')]", "keyData": "[parameters('moodleCommon').sshPublicKey]" } ] } } }, "storageProfile": { "imageReference": "[parameters('moodleCommon').osType]", "osDisk": { "createOption": "FromImage", "managedDisk": { "storageAccountType": "[parameters('moodleCommon').osDiskStorageType]" }, "name": "[parameters('moodleCommon').ctlrVmName]" }, "dataDisks": "[take(variables('nfsDiskArray'),if(equals(parameters('moodleCommon').fileServerType,'nfs'), parameters('moodleCommon').fileServerDiskCount, 0))]" } }, "tags": { "displayName": "Controller Virtual Machine" } }, { "condition": "[parameters('moodleCommon').applyScriptsSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/', parameters('moodleCommon').ctlrVmName)]" ], "name": "[concat(parameters('moodleCommon').ctlrVmName,'-ScriptProcessor')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl, 'controllersetup.json', parameters('moodleCommon').artifactsSasToken)]" } } }, { "condition": "[parameters('moodleCommon').azureBackupSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/',parameters('moodleCommon').ctlrVmName)]" ], "name": "[concat(parameters('moodleCommon').ctlrVmName,'-Backup')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vmName": { "value": "[parameters('moodleCommon').ctlrVmName]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'recoveryservicesEnlist.json',parameters('moodleCommon').artifactsSasToken)]" } } } ], "variables": { "documentation01": "This sub-template drives the controller/jump-box which is used as the access-point for other moodle VM's ", "documentation02": "It expects certain values in the 'common' datastructure.", "documentation03": " vnetName - name of virtual network", "documentation04": " subnetWeb - name of subnet for controller (and vm scale set)", "documentation06": " ctlrPipName - name of Public IP address for the controller (note that none of the other VM's get a PIP - just the controller", "documentation07": " ctlrNicName - name of the network interface (all VM's must hae a nic) to crate, tied to the public IP address", "documentation08": " ctlrNsgName - name of the network security group, regulating access to/from the controller", "documentation09": "This sub-template calls other sub-templates", "documentation10": " controllerconfig - conditionally applies post-deployment script on the VM", "documentation18": " recoveryservicesEnlist - conditionally enlists the VM into the backup regimen", "nicRef": "[resourceId('Microsoft.Network/networkInterfaces', parameters('moodleCommon').ctlrNicName)]", "nsgRef": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('moodleCommon').ctlrNsgName)]", "singleQuote": "'", "copy": [ { "name": "nfsDiskArray", "count": 8, "input": { "managedDisk": { "storageAccountType": "Premium_LRS" }, "diskSizeGB": "[parameters('moodleCommon').fileServerDiskSize]", "lun": "[copyIndex('nfsDiskArray')]", "createOption": "Empty" } } ] }, "outputs": { "controllerIP": { "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses', parameters('moodleCommon').ctlrPipName), '2017-10-01').ipAddress]", "type": "string" } } } ================================================ FILE: nested/controllersetup.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" } }, "resources": [ { "type": "Microsoft.Compute/virtualMachines/extensions", "apiVersion": "2017-03-30", "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('moodleCommon').ctlrVmName,'/','install_moodle')]", "properties": { "autoUpgradeMinorVersion": true, "publisher": "Microsoft.Azure.Extensions", "settings": { "fileUris": [ "[variables('scriptUri')]", "[parameters('moodleCommon').commonFunctionsScriptUri]" ] }, "protectedSettings":{ "commandToExecute": "[concat('bash ', parameters('moodleCommon').moodleInstallScriptFilename, ' ', parameters('moodleCommon').moodleOnAzureConfigsJsonPath)]" }, "type": "CustomScript", "typeHandlerVersion": "2.0" }, "tags": { "displayName": "install_moodle" } } ], "variables": { "documentation01": "This sub-template applies a specific post-deployment script to the controller vm", "documentation02": "It expects certain values in the 'common' datastructure.", "documentation03": " scriptLocation - web URI", "documentation04": " moodleInstallScriptFilename - name of script file", "documentation05": " siteURL - URL of the website", "documentation06": " gfsNameRoot - nameroot of gluster farm - note that the code applies a 0 to get to the first node", "documentation07": " ctlrVmName - name of the controller/jumpb ox VM", "documentation08": " dbServerType - postgres or mysql", "documentation09": " moodleDbName - database name for moodle", "documentation10": " moodleDbUser - database user for moodle", "documentation11": " moodleDbPass - database password for moodleDbUser", "documentation12": " moodleAdminPass - password for moodle admin user", "documentation13": " mssqlDbServiceObjectiveName - MS SQL porformance tier.", "documentation14": " mssqlDbEdition - MS SQL edition tier", "documentation15": " mssqlDbSize - MS SQL database size", "scriptUri": "[concat(parameters('moodleCommon').scriptLocation,parameters('moodleCommon').moodleInstallScriptFilename,parameters('moodleCommon').artifactsSasToken)]" } } ================================================ FILE: nested/db-mssql.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01-preview/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "lbPubIp": { "metadata": { "description": "Public IP address of the deployed load balancer" }, "type": "string" }, "lbOut001PubIp": { "metadata": { "description": "Outgoing Public IP address of the deployed load balancer" }, "type": "string" }, "lbOut002PubIp": { "metadata": { "description": "Outgoing Public IP address of the deployed load balancer" }, "type": "string" }, "ctlrPubIp": { "metadata": { "description": "Public IP address of the deployed controller VM" }, "type": "string" }, "subnetIdDb": { "metadata": { "description": "Azure resource ID of the subnet where the Db is to be deployed.(if deployed in vnet)" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Sql/servers", "apiVersion": "2015-05-01-preview", "kind": "", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').serverName]", "properties": { "administratorLogin": "[parameters('moodleCommon').dbLogin]", "administratorLoginPassword": "[parameters('moodleCommon').dbLoginPassword]", "version": "[parameters('moodleCommon').mssqlVersion]" }, "resources": [ { "apiVersion": "2014-04-01", "dependsOn": [ "[concat('Microsoft.Sql/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "mssql-firewall-allow-lb", "properties": { "startIpAddress": "[parameters('lbPubIp')]", "endIpAddress": "[parameters('lbPubIp')]" }, "type": "firewallRules" }, { "apiVersion": "2014-04-01", "dependsOn": [ "[concat('Microsoft.Sql/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "mssql-firewall-allow-lb-out001", "properties": { "startIpAddress": "[parameters('lbOut001PubIp')]", "endIpAddress": "[parameters('lbOut001PubIp')]" }, "type": "firewallRules" }, { "apiVersion": "2014-04-01", "dependsOn": [ "[concat('Microsoft.Sql/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "mssql-firewall-allow-lb-out002", "properties": { "startIpAddress": "[parameters('lbOut002PubIp')]", "endIpAddress": "[parameters('lbOut002PubIp')]" }, "type": "firewallRules" }, { "apiVersion": "2014-04-01", "dependsOn": [ "[concat('Microsoft.Sql/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "mssql-firewall-allow-ctlr", "properties": { "startIpAddress": "[parameters('ctlrPubIp')]", "endIpAddress": "[parameters('ctlrPubIp')]" }, "type": "firewallRules" } ] } ], "outputs": { "dbFQDN": { "type": "string", "value": "[reference(parameters('moodleCommon').serverName).fullyQualifiedDomainName]" } }, "variables": { "documentation1": "This sub-template creates a mssql server. It expects certain values in the 'common' datastructure.", "documentation10": " serverName - Mssql server name", "documentation11": " mssqlVersion - Mssql version", "documentation2": " administratorLogin - Mssql admin username", "documentation3": " administratorLoginPassword - Mssql admin password", "documentation4": " location - Mssql server location" } } ================================================ FILE: nested/db-mysql.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "lbPubIp": { "metadata": { "description": "Public IP address of the deployed load balancer" }, "type": "string" }, "lbOut001PubIp": { "metadata": { "description": "Outgoing Public IP address of the deployed load balancer" }, "type": "string" }, "lbOut002PubIp": { "metadata": { "description": "Outgoing Public IP address of the deployed load balancer" }, "type": "string" }, "ctlrPubIp": { "metadata": { "description": "Public IP address of the deployed controller VM" }, "type": "string" }, "subnetIdDb": { "metadata": { "description": "Azure resource ID of the subnet where the Db is to be deployed.(if deployed in vnet)" }, "type": "string" } }, "resources": [ { "type": "Microsoft.DBforMySQL/servers", "apiVersion": "2017-12-01", "kind": "", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').serverName]", "properties": { "administratorLogin": "[parameters('moodleCommon').dbLogin]", "administratorLoginPassword": "[parameters('moodleCommon').dbLoginPassword]", "sslEnforcement": "[parameters('moodleCommon').sslEnforcement]", "storageProfile": { "storageMB": "[mul(parameters('moodleCommon').mysqlPgresStgSizeGB, 1024)]", "backupRetentionDays": "35", "geoRedundantBackup": "Enabled" }, "version": "[parameters('moodleCommon').mysqlVersion]" }, "sku": { "capacity": "[parameters('moodleCommon').mysqlPgresVcores]", "name": "[parameters('moodleCommon').mysqlPgresSkuName]", "tier": "[parameters('moodleCommon').mysqlPgresSkuTier]", "family": "[parameters('moodleCommon').mysqlPgresSkuHwFamily]" }, "resources": [ { "apiVersion": "2017-12-01", "dependsOn": [ "[concat('Microsoft.DBforMySQL/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "mysql-firewall-allow-lb", "properties": { "startIpAddress": "[parameters('lbPubIp')]", "endIpAddress": "[parameters('lbPubIp')]" }, "type": "firewallRules" }, { "apiVersion": "2017-12-01", "dependsOn": [ "[concat('Microsoft.DBforMySQL/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "mysql-firewall-allow-lb-out001", "properties": { "startIpAddress": "[parameters('lbOut001PubIp')]", "endIpAddress": "[parameters('lbOut001PubIp')]" }, "type": "firewallRules" }, { "apiVersion": "2017-12-01", "dependsOn": [ "[concat('Microsoft.DBforMySQL/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "mysql-firewall-allow-lb-out002", "properties": { "startIpAddress": "[parameters('lbOut002PubIp')]", "endIpAddress": "[parameters('lbOut002PubIp')]" }, "type": "firewallRules" }, { "apiVersion": "2017-12-01", "dependsOn": [ "[concat('Microsoft.DBforMySQL/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "mysql-firewall-allow-ctlr", "properties": { "startIpAddress": "[parameters('ctlrPubIp')]", "endIpAddress": "[parameters('ctlrPubIp')]" }, "type": "firewallRules" } ] } ], "outputs": { "dbFQDN": { "type": "string", "value": "[reference(parameters('moodleCommon').serverName).fullyQualifiedDomainName]" } }, "variables": { "documentation1": "This sub-template creates a mysql server. It expects certain values in the 'common' datastructure.", "documentation10": " serverName - Mysql server name", "documentation11": " mysqlVersion - Mysql version", "documentation2": " administratorLogin - mysql admin username", "documentation3": " administratorLoginPassword - mysql admin password", "documentation4": " location - Mysql server location", "documentation5": " mysqlPgresVcores - Mysql database trasaction units", "documentation7": " mysqlPgresSkuName - Mysql sku name", "documentation8": " mysqlPgresStgSizeGB - Mysql sku size in mb", "documentation9": " mysqlPgresSkuTier - Mysql sku tier", "documentationA": " mysqlPgresSkuHwFamily - Mysql sku hardware family" } } ================================================ FILE: nested/db-mysqlflex.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "lbPubIp": { "metadata": { "description": "Public IP address of the deployed load balancer" }, "type": "string" }, "lbOut001PubIp": { "metadata": { "description": "Outgoing Public IP address of the deployed load balancer" }, "type": "string" }, "lbOut002PubIp": { "metadata": { "description": "Outgoing Public IP address of the deployed load balancer" }, "type": "string" }, "ctlrPubIp": { "metadata": { "description": "Public IP address of the deployed controller VM" }, "type": "string" }, "subnetIdDb": { "metadata": { "description": "Azure resource ID of the subnet where the Db is to be deployed.(if deployed in vnet)" }, "type": "string" } }, "resources": [ { "type": "Microsoft.DBforMySQL/flexibleServers", "apiVersion": "2021-05-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').serverName]", "sku": { "name": "[parameters('moodleCommon').mysqlflexSkuName]", "tier": "[parameters('moodleCommon').mysqlflexSkuTier]" }, "properties": { "version": "[parameters('moodleCommon').mysqlVersion]", "administratorLogin": "[parameters('moodleCommon').dbLogin]", "administratorLoginPassword": "[parameters('moodleCommon').dbLoginPassword]", "availabilityZone": "[parameters('moodleCommon').mysqlflexAvailabilityZone]", "highAvailability": { "mode": "[parameters('moodleCommon').mysqlflexHaEnabled]", "standbyAvailabilityZone": "[parameters('moodleCommon').mysqlflexStandbyAvailabilityZone]" }, "Storage": { "storageSizeGB": "[parameters('moodleCommon').mysqlflexStgSizeGiB]", "iops": "[parameters('moodleCommon').mysqlflexStgIops]", "autogrow": "[parameters('moodleCommon').mysqlflexStgAutogrow]" }, "network": "[if(parameters('moodleCommon').vnetDbDeploySwitch, variables('vnetNetworkData'), createObject())]", "Backup": { "backupRetentionDays": 35, "geoRedundantBackup": "Enabled" } } }, { "type": "Microsoft.DBforMySQL/flexibleServers/configurations", "apiVersion": "2021-05-01", "name": "[concat(parameters('moodleCommon').serverName, '/require_secure_transport')]", "dependsOn": [ "[concat('Microsoft.DBforMySQL/flexibleServers/', parameters('moodleCommon').serverName)]" ], "properties": { "value": "[parameters('moodleCommon').mysqlflexRequireSecureTransport]", "source": "user-override" } }, { "condition": "[not(parameters('moodleCommon').vnetDbDeploySwitch)]", "type": "Microsoft.DBforMySQL/flexibleServers/firewallRules", "apiVersion": "2021-05-01", "name": "[concat(parameters('moodleCommon').serverName, '/mysqlflex-firewall-allow-lb')]", "dependsOn": [ "[concat('Microsoft.DBforMySQL/flexibleservers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "properties": { "startIpAddress": "[parameters('lbPubIp')]", "endIpAddress": "[parameters('lbPubIp')]" } }, { "condition": "[not(parameters('moodleCommon').vnetDbDeploySwitch)]", "type": "Microsoft.DBforMySQL/flexibleServers/firewallRules", "apiVersion": "2021-05-01", "name": "[concat(parameters('moodleCommon').serverName, '/mysqlflex-firewall-allow-lb-out001')]", "dependsOn": [ "[concat('Microsoft.DBforMySQL/flexibleservers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "properties": { "startIpAddress": "[parameters('lbOut001PubIp')]", "endIpAddress": "[parameters('lbOut001PubIp')]" } }, { "condition": "[not(parameters('moodleCommon').vnetDbDeploySwitch)]", "type": "Microsoft.DBforMySQL/flexibleServers/firewallRules", "apiVersion": "2021-05-01", "name": "[concat(parameters('moodleCommon').serverName, '/mysqlflex-firewall-allow-lb-out002')]", "dependsOn": [ "[concat('Microsoft.DBforMySQL/flexibleservers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "properties": { "startIpAddress": "[parameters('lbOut002PubIp')]", "endIpAddress": "[parameters('lbOut002PubIp')]" } }, { "condition": "[not(parameters('moodleCommon').vnetDbDeploySwitch)]", "type": "Microsoft.DBforMySQL/flexibleServers/firewallRules", "apiVersion": "2021-05-01", "name": "[concat(parameters('moodleCommon').serverName, '/mysqlflex-firewall-allow-ctlr')]", "dependsOn": [ "[concat('Microsoft.DBforMySQL/flexibleservers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "properties": { "startIpAddress": "[parameters('ctlrPubIp')]", "endIpAddress": "[parameters('ctlrPubIp')]" } } ], "variables": { "vnetNetworkData": { "delegatedSubnetResourceId": "[parameters('subnetIdDb')]", "privateDnsZoneResourceId": "[resourceId('Microsoft.Network/privateDnsZones', parameters('moodleCommon').mysqlflexPrivateDnsZoneName)]" } }, "outputs": { "dbFQDN": { "type": "string", "value": "[reference(parameters('moodleCommon').serverName).fullyQualifiedDomainName]" } } } ================================================ FILE: nested/db-postgres.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "lbPubIp": { "metadata": { "description": "Public IP address of the deployed load balancer" }, "type": "string" }, "lbOut001PubIp": { "metadata": { "description": "Outgoing Public IP address of the deployed load balancer" }, "type": "string" }, "lbOut002PubIp": { "metadata": { "description": "Outgoing Public IP address of the deployed load balancer" }, "type": "string" }, "ctlrPubIp": { "metadata": { "description": "Public IP address of the deployed controller VM" }, "type": "string" }, "subnetIdDb": { "metadata": { "description": "Azure resource ID of the subnet where the Db is to be deployed.(if deployed in vnet)" }, "type": "string" } }, "resources": [ { "type": "Microsoft.DBforPostgreSQL/servers", "apiVersion": "2017-12-01", "kind": "", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').serverName]", "properties": { "administratorLogin": "[parameters('moodleCommon').dbLogin]", "administratorLoginPassword": "[parameters('moodleCommon').dbLoginPassword]", "sslEnforcement": "[parameters('moodleCommon').sslEnforcement]", "storageProfile": { "storageMB": "[mul(parameters('moodleCommon').mysqlPgresStgSizeGB, 1024)]", "backupRetentionDays": "35", "geoRedundantBackup": "Enabled" }, "version": "[parameters('moodleCommon').postgresVersion]" }, "sku": { "capacity": "[parameters('moodleCommon').mysqlPgresVcores]", "name": "[parameters('moodleCommon').mysqlPgresSkuName]", "tier": "[parameters('moodleCommon').mysqlPgresSkuTier]", "family": "[parameters('moodleCommon').mysqlPgresSkuHwFamily]" }, "resources": [ { "apiVersion": "2017-12-01", "dependsOn": [ "[concat('Microsoft.DBforPostgreSQL/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "postgres-firewall-allow-lb", "properties": { "startIpAddress": "[parameters('lbPubIp')]", "endIpAddress": "[parameters('lbPubIp')]" }, "type": "firewallRules" }, { "apiVersion": "2017-12-01", "dependsOn": [ "[concat('Microsoft.DBforPostgreSQL/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "postgres-firewall-allow-lb-out001", "properties": { "startIpAddress": "[parameters('lbOut001PubIp')]", "endIpAddress": "[parameters('lbOut001PubIp')]" }, "type": "firewallRules" }, { "apiVersion": "2017-12-01", "dependsOn": [ "[concat('Microsoft.DBforPostgreSQL/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "postgres-firewall-allow-lb-out002", "properties": { "startIpAddress": "[parameters('lbOut002PubIp')]", "endIpAddress": "[parameters('lbOut002PubIp')]" }, "type": "firewallRules" }, { "apiVersion": "2017-12-01", "dependsOn": [ "[concat('Microsoft.DBforPostgreSQL/servers/', parameters('moodleCommon').serverName)]" ], "location": "[parameters('moodleCommon').location]", "name": "postgres-firewall-allow-ctlr", "properties": { "startIpAddress": "[parameters('ctlrPubIp')]", "endIpAddress": "[parameters('ctlrPubIp')]" }, "type": "firewallRules" } ] } ], "outputs": { "dbFQDN": { "type": "string", "value": "[reference(parameters('moodleCommon').serverName).fullyQualifiedDomainName]" } }, "variables": { "documentation1": "This sub-template creates a postgresql server. It expects certain values in the 'common' datastructure.", "documentation10": " serverName - Postgresql server name", "documentation11": " postgresVersion - Postgresql version", "documentation2": " administratorLogin - postgresql admin username", "documentation3": " administratorLoginPassword - postgresql admin password", "documentation4": " location - Postgresql server location", "documentation5": " mysqlPgresVcores - Postgresql database trasaction units", "documentation7": " mysqlPgresSkuName - Postgresql sku name", "documentation8": " mysqlPgresStgSizeGB - Postgresql sku size in mb", "documentation9": " mysqlPgresSkuTier - Postgresql sku tier", "documentationA": " mysqlPgresSkuHwFamily - Mysql sku hardware family" } } ================================================ FILE: nested/gluster.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "subnetIdSan": { "metadata": { "description": "Azure resource ID of the subnet where this gluster cluster is to be deployed" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Compute/availabilitySets", "apiVersion": "2017-03-30", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').gfxAvailabilitySetName]", "properties": { "platformFaultDomainCount": 2, "platformUpdateDomainCount": 5 }, "sku": { "name": "Aligned" }, "tags": { "displayName": "Gluster Availability Set" } }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "copy": { "count": "[parameters('moodleCommon').fileServerVmCount]", "name": "vmloop" }, "dependsOn": [ "[concat('Microsoft.Compute/availabilitySets/',parameters('moodleCommon').gfxAvailabilitySetName)]" ], "name": "[concat('glustervm',copyindex())]", "properties": { "mode": "Incremental", "parameters": { "counter": { "value": "[copyindex()]" }, "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "subnetIdSan": { "value": "[parameters('subnetIdSan')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'glustervm.json',parameters('moodleCommon').artifactsSasToken)]" } } } ], "variables": { "documentation1": "This sub-template drives the gluster (scale-out network-attached storage file system) creation process.", "documentation2": "It expects certain values in the 'common' datastructure.", "documentation4": " gfxAvailabilitySetName - name of availability set for the gluster farm", "documentation5": " fileServerVmCount - number of nodes to create", "documentation6": "This sub-template calls other sub-templates", "documentation7": " glustervm - number of nodes in the gluster farm" } } ================================================ FILE: nested/glustervm.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "counter": { "metadata": { "description": "from the copyindex function of calling template" }, "type": "int" }, "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "subnetIdSan": { "metadata": { "description": "Azure resource ID of the subnet where this gluster cluster is to be deployed" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2017-10-01", "location": "[parameters('moodleCommon').location]", "name": "[variables('nicName')]", "properties": { "ipConfigurations": [ { "name": "ipcfggfs", "properties": { "privateIPAllocationMethod": "Dynamic", "subnet": { "id": "[parameters('subnetIdSan')]" } } } ], "enableAcceleratedNetworking": "[parameters('moodleCommon').enableAccelNwForOtherVmsSwitch]" }, "tags": { "displayName": "Gluster VM NIC" } }, { "type": "Microsoft.Compute/virtualMachines", "apiVersion": "2017-03-30", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" ], "location": "[parameters('moodleCommon').location]", "name": "[variables('vmName')]", "properties": { "availabilitySet": { "id": "[variables('asRef')]" }, "hardwareProfile": { "vmSize": "[parameters('moodleCommon').fileServerVmSku]" }, "networkProfile": { "networkInterfaces": [ { "id": "[variables('nicRef')]" } ] }, "osProfile": { "adminUsername": "[parameters('moodleCommon').sshUsername]", "computerName": "[variables('vmName')]", "linuxConfiguration": { "disablePasswordAuthentication": true, "ssh": { "publicKeys": [ { "path": "[concat('/home/', parameters('moodleCommon').sshUsername, '/.ssh/authorized_keys')]", "keyData": "[parameters('moodleCommon').sshPublicKey]" } ] } } }, "storageProfile": { "imageReference": "[parameters('moodleCommon').osType]", "osDisk": { "createOption": "FromImage", "managedDisk": { "storageAccountType": "[parameters('moodleCommon').osDiskStorageType]" }, "name": "[variables('vmName')]" }, "copy": [ { "name": "dataDisks", "count": "[parameters('moodleCommon').fileServerDiskCount]", "input": { "managedDisk": { "storageAccountType": "Premium_LRS" }, "diskSizeGB": "[parameters('moodleCommon').fileServerDiskSize]", "lun": "[copyIndex('dataDisks')]", "createOption": "Empty" } } ] } }, "tags": { "displayName": "Gluster Virtual Machine" } }, { "condition": "[parameters('moodleCommon').applyScriptsSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/',variables('vmName'))]" ], "name": "[concat(variables('vmName'),'-ScriptProcessor')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vmName": { "value": "[ variables('vmName')]" }, "vmNumber": { "value": "[parameters('counter')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'glustervmsetup.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "condition": "[parameters('moodleCommon').azureBackupSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/',variables('vmName'))]" ], "name": "[concat(variables('vmName'),'-Backup')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vmName": { "value": "[variables('vmName')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'recoveryservicesEnlist.json',parameters('moodleCommon').artifactsSasToken)]" } } } ], "variables": { "asRef": "[resourceId('Microsoft.Compute/availabilitySets', parameters('moodleCommon').gfxAvailabilitySetName)]", "documentation01": "This sub-template create the nodes of the gluster farm", "documentation02": "It expects certain values in the 'common' datastructure.", "documentation04": " gfxAvailabilitySetName - name of availability set for the gluster farm", "documentation05": " vnetName - name of virtual network", "documentation06": " subnetSan - name of subnet for gluster", "documentation07": " gfsNameRoot - nameroot for the gluster nodes - combined with counter to get actual name of each node - disk and nic follow the naming scheme", "documentation08": " fileServerVmSku - VM instance size for gluster nodes", "documentation09": " sshUsername - OS accountusername", "documentation10": " osType - an array of value that specifies the type of VM", "documentation15": "This sub-template calls other sub-templates", "documentation17": " glustervmconfig - conditionally applies post-deployment script on the VM", "documentation18": " recoveryservicesEnlist - conditionally enlists the VM into the backup regimen", "documentation19": " fileServerDiskCount - Number of disks to raid0 for the gluster mount", "documentation20": " fileServerDiskSize - Size per disk for gluster", "nicName": "[concat(variables('vmName'),'-nic')]", "nicRef": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]", "vmName": "[concat(parameters('moodleCommon').gfsNameRoot,parameters('counter'))]" } } ================================================ FILE: nested/glustervmsetup.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "vmName": { "metadata": { "description": "Name of VM to process script - not actually used" }, "type": "string" }, "vmNumber": { "metadata": { "description": "Number of the VM in the pool" }, "type": "int" } }, "resources": [ { "type": "Microsoft.Compute/virtualMachines/extensions", "apiVersion": "2017-03-30", "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('vmName'),'/','install_gluster')]", "properties": { "publisher": "Microsoft.Azure.Extensions", "settings": { "fileUris": [ "[variables('scriptUri')]" ] }, "protectedSettings":{ "commandToExecute": "[variables('cmdExec')]" }, "type": "CustomScript", "typeHandlerVersion": "2.0" }, "tags": { "displayName": "GfsVmExtension" } } ], "variables": { "cmdExec": "[concat('bash ', parameters('moodleCommon').glusterScriptFilename, ' ', parameters('moodleCommon').gfsNameRoot, ' ', parameters('moodleCommon').subnetSanPrefix, ' data ', parameters('vmNumber'), ' ', parameters('moodleCommon').fileServerVmCount)]", "documentation01": "This sub-template applies a specific post-deployment script to the gluster vms", "documentation02": "It expects certain values in the 'common' datastructure.", "documentation03": " scriptLocation - partial web URI (equivalent to folder)", "documentation04": " glusterScriptFilename - name of script file", "documentation06": " gfsNameRoot - nameroot of gluster farm - note that the code applies a vmNumber to get to the specific node", "documentation07": " fileServerVmCount - number of gluster VMs", "scriptUri": "[concat(parameters('moodleCommon').scriptLocation,parameters('moodleCommon').glusterScriptFilename,parameters('moodleCommon').artifactsSasToken)]" } } ================================================ FILE: nested/network-subnets.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "vnetName": { "metadata": { "description": "The name of the vnet where subnets should be created" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Network/virtualNetworks/subnets", "apiVersion": "2017-10-01", "name": "[concat(parameters('vnetName'), '/', parameters('moodleCommon').subnetWeb)]", "location": "[parameters('moodleCommon').location]", "properties": { "addressPrefix": "[parameters('moodleCommon').subnetWebRange]" } }, { "type": "Microsoft.Network/virtualNetworks/subnets", "apiVersion": "2017-10-01", "name": "[concat(parameters('vnetName'), '/', parameters('moodleCommon').subnetSan)]", "location": "[parameters('moodleCommon').location]", "dependsOn": [ "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('moodleCommon').subnetWeb)]" ], "properties": { "addressPrefix": "[parameters('moodleCommon').subnetSanRange]" } }, { "type": "Microsoft.Network/virtualNetworks/subnets", "apiVersion": "2017-10-01", "name": "[concat(parameters('vnetName'), '/', parameters('moodleCommon').subnetRedis)]", "location": "[parameters('moodleCommon').location]", "dependsOn": [ "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('moodleCommon').subnetSan)]" ], "properties": { "addressPrefix": "[parameters('moodleCommon').subnetRedisRange]" } }, { "type": "Microsoft.Network/virtualNetworks/subnets", "apiVersion": "2017-10-01", "name": "[concat(parameters('vnetName'), '/', parameters('moodleCommon').subnetElastic)]", "location": "[parameters('moodleCommon').location]", "dependsOn": [ "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('moodleCommon').subnetRedis)]" ], "properties": { "addressPrefix": "[parameters('moodleCommon').subnetElasticRange]" } }, { "type": "Microsoft.Network/virtualNetworks/subnets", "apiVersion": "2017-10-01", "name": "[concat(parameters('vnetName'), '/', parameters('moodleCommon').subnetTika)]", "location": "[parameters('moodleCommon').location]", "dependsOn": [ "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('moodleCommon').subnetElastic)]" ], "properties": { "addressPrefix": "[parameters('moodleCommon').subnetTikaRange]" } }, { "condition": "[parameters('moodleCommon').vnetGwDeploySwitch]", "type": "Microsoft.Network/virtualNetworks/subnets", "apiVersion": "2017-10-01", "name": "[concat(parameters('vnetName'), '/', parameters('moodleCommon').subnetGateway)]", "location": "[parameters('moodleCommon').location]", "dependsOn": [ "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('moodleCommon').subnetTika)]" ], "properties": { "addressPrefix": "[parameters('moodleCommon').subnetGatewayRange]" } }, { "type": "Microsoft.Network/virtualNetworks/subnets", "apiVersion": "2017-10-01", "name": "[concat(parameters('vnetName'), '/', parameters('moodleCommon').subnetAppGw)]", "location": "[parameters('moodleCommon').location]", "dependsOn": [ "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('moodleCommon').subnetGateway)]" ], "properties": { "addressPrefix": "[parameters('moodleCommon').subnetAppGwRange]" } }, { "condition": "[and(parameters('moodleCommon').vnetDbDeploySwitch, equals(parameters('moodleCommon').dbServerType, 'mysqlflex'))]", "type": "Microsoft.Network/virtualNetworks/subnets", "apiVersion": "2021-05-01", "name": "[concat(parameters('vnetName'),'/',parameters('moodleCommon').subnetDb)]", "location": "[parameters('moodleCommon').location]", "dependsOn": [ "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('moodleCommon').subnetAppGw)]" ], "properties": { "addressPrefix": "[parameters('moodleCommon').subnetDbRange]", "delegations": [ { "name": "MySQLflexibleServers", "properties": { "serviceName": "Microsoft.DBforMySQL/flexibleServers" } } ] } } ], "variables": { "documentation01": "This sub-template creates various subnets needed for various components of the cluster.", "documentation02": "This needs to be done on a separate nested template, in order to allow to create subnets on a customer's BYO-vnet, which may be on a different subscription and/or on a different resource group.", "documentation03": "The subnets are created one-by-one (using the dependsOn's), to avoid ARM's 'Another operation on this or dependent resource is in progress' failures" } } ================================================ FILE: nested/network-vnet-ddos.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "vnetName": { "metadata": { "description": "The name of the vnet to which the DDoS protection plan shoud be associated" }, "type": "string" }, "vNetAddressSpace": { "metadata": { "description": "The vNet Address Space to which the DDoS protection plan shoud be associated" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Network/ddosProtectionPlans", "apiVersion": "2018-02-01", "condition": "[parameters('moodleCommon').ddosSwitch]", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').ddosPlanName]" }, { "type": "Microsoft.Network/virtualNetworks", "apiVersion": "2018-02-01", "dependsOn": [ "[resourceId('Microsoft.Network/ddosProtectionPlans', parameters('moodleCommon').ddosPlanName)]" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('vnetName')]", "properties": { "addressSpace": { "addressPrefixes": [ "[parameters('vNetAddressSpace')]" ] }, "ddosProtectionPlan": { "id": "[resourceId('Microsoft.Network/ddosProtectionPlans', parameters('moodleCommon').ddosPlanName)]" }, "enableDdosProtection": "[parameters('moodleCommon').ddosSwitch]" } } ], "variables": { "documentation01": "This sub-template creates a Azure DDoS protection plan and link it to a vnet" } } ================================================ FILE: nested/network-vnet-privateDnsZone.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "vnetId": { "metadata": { "description": "The resource id of the vnet to which the private DNS Zone shoud be associated" }, "type": "string" } }, "resources": [ { "condition": "[and(parameters('moodleCommon').vnetDbDeploySwitch, equals(parameters('moodleCommon').dbServerType, 'mysqlflex'))]", "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2018-09-01", "name": "[parameters('moodleCommon').mysqlflexPrivateDnsZoneName]", "location": "global", "tags": {}, "properties": {} }, { "condition": "[and(parameters('moodleCommon').vnetDbDeploySwitch, equals(parameters('moodleCommon').dbServerType, 'mysqlflex'))]", "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2020-06-01", "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', parameters('moodleCommon').mysqlflexPrivateDnsZoneName)]" ], "name": "[concat(parameters('moodleCommon').mysqlflexPrivateDnsZoneName, '/', uniqueString(parameters('vnetId')))]", "location": "global", "properties": { "virtualNetwork": { "id": "[parameters('vnetId')]" }, "registrationEnabled": false } } ], "variables": { "documentation01": "This sub-template creates private DNS zones and link it to a vnet" } } ================================================ FILE: nested/network-vnet.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" } }, "resources": [ { "type": "Microsoft.Network/virtualNetworks", "apiVersion": "2017-10-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').vnetName]", "properties": { "addressSpace": { "addressPrefixes": [ "[concat(parameters('moodleCommon').vNetAddressSpace,'/16')]" ] } } } ], "variables": { "documentation01": "This sub-template creates a virtual network when no customer BYO-vnet is specified" } } ================================================ FILE: nested/network.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" } }, "resources": [ { "condition": "[equals(parameters('moodleCommon').customVnetId, '')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "name": "vnetTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'network-vnet.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "apiVersion": "2018-02-01", "condition": "[parameters('moodleCommon').ddosSwitch]", "dependsOn": [ "Microsoft.Resources/deployments/vnetTemplate" ], "name": "ddosTemplate", "subscriptionId": "[variables('vnetSub')]", "resourceGroup": "[variables('vnetRg')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vnetName": { "value": "[variables('vnetName')]" }, "vNetAddressSpace": { "value": "[if(equals(parameters('moodleCommon').customVnetId, ''), concat(parameters('moodleCommon').vNetAddressSpace,'/16'), reference(parameters('moodleCommon').customVnetId, '2017-10-01').addressSpace.addressPrefixes[0])]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'network-vnet-ddos.json',parameters('moodleCommon').artifactsSasToken)]" } }, "type": "Microsoft.Resources/deployments" }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2020-06-01", "dependsOn": [ "Microsoft.Resources/deployments/vnetTemplate" ], "name": "privateDnsZoneTemplate", "subscriptionId": "[variables('vnetSub')]", "resourceGroup": "[variables('vnetRg')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vnetId": { "value": "[variables('vnetId')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'network-vnet-privateDnsZone.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/vnetTemplate", "Microsoft.Resources/deployments/ddosTemplate" ], "name": "subnetTemplate", "subscriptionId": "[variables('vnetSub')]", "resourceGroup": "[variables('vnetRg')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vnetName": { "value": "[variables('vnetName')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'network-subnets.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "condition": "[parameters('moodleCommon').vnetGwDeploySwitch]", "type": "Microsoft.Network/publicIPAddresses", "apiVersion": "2019-11-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').gatewayPublicIPName]", "properties": { "publicIPAllocationMethod": "Dynamic" }, "tags": { "displayName": "Virtual network gateway Public IP" } }, { "condition": "[parameters('moodleCommon').vnetGwDeploySwitch]", "type": "Microsoft.Network/virtualNetworkGateways", "apiVersion": "2017-10-01", "dependsOn": [ "[resourceId('Microsoft.Network/publicIPAddresses', parameters('moodleCommon').gatewayPublicIPName)]", "Microsoft.Resources/deployments/subnetTemplate" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').gatewayName]", "properties": { "activeActive": false, "enableBgp": false, "gatewayType": "[parameters('moodleCommon').gatewayType]", "ipConfigurations": [ { "name": "vnet-Gateway-Config", "properties": { "privateIPAllocationMethod": "Dynamic", "publicIPAddress": { "id": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('moodleCommon').gatewayPublicIPName)]" }, "subnet": { "id": "[variables('subnetIdGateway')]" } } } ], "sku": { "name": "VpnGw1", "tier": "VpnGw1", "capacity": 2 }, "vpnType": "[parameters('moodleCommon').vpnType]" } }, { "condition": "[not(equals(parameters('moodleCommon').httpsTermination, 'AppGw'))]", "type": "Microsoft.Network/publicIPAddresses", "sku": { "name": "[parameters('moodleCommon').lbSku]", "tier": "[parameters('moodleCommon').lbTier]" }, "apiVersion": "2019-11-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').lbPipName]", "properties": { "dnsSettings": { "domainNameLabel": "[parameters('moodleCommon').lbName]" }, "publicIPAllocationMethod": "Static" }, "tags": { "displayName": "Load Balancer Public IP" } }, { "condition": "[not(equals(parameters('moodleCommon').httpsTermination, 'AppGw'))]", "type": "Microsoft.Network/publicIPAddresses", "sku": { "name": "[parameters('moodleCommon').lbSku]", "tier": "[parameters('moodleCommon').lbTier]" }, "apiVersion": "2019-11-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').lbOutPipName001]", "properties": { "dnsSettings": { "domainNameLabel": "[parameters('moodleCommon').lbOutName001]" }, "publicIPAllocationMethod": "Static" }, "tags": { "displayName": "Load Balancer Outbound Public IP 001" } }, { "condition": "[not(equals(parameters('moodleCommon').httpsTermination, 'AppGw'))]", "type": "Microsoft.Network/publicIPAddresses", "sku": { "name": "[parameters('moodleCommon').lbSku]", "tier": "[parameters('moodleCommon').lbTier]" }, "apiVersion": "2019-11-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').lbOutPipName002]", "properties": { "dnsSettings": { "domainNameLabel": "[parameters('moodleCommon').lbOutName002]" }, "publicIPAllocationMethod": "Static" }, "tags": { "displayName": "Load Balancer Outbound Public IP 002" } }, { "condition": "[equals(parameters('moodleCommon').httpsTermination, 'AppGw')]", "type": "Microsoft.Network/publicIPAddresses", "apiVersion": "2019-11-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').appGwPipName]", "sku" : { "name" : "[if(endswith(parameters('moodleCommon').appGwSkuName,'v2'),'Standard','Basic')]" }, "properties": { "dnsSettings": { "domainNameLabel": "[parameters('moodleCommon').appGwName]" }, "publicIPAllocationMethod": "[if(endswith(parameters('moodleCommon').appGwSkuName,'v2'),'Static','Dynamic')]" } }, { "type": "Microsoft.Network/publicIPAddresses", "apiVersion": "2019-11-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').ctlrPipName]", "properties": { "dnsSettings": { "domainNameLabel": "[parameters('moodleCommon').ctlrPipName]" }, "publicIPAllocationMethod": "Static" }, "tags": { "displayName": "Controller VM Public IP" } }, { "condition": "[not(equals(parameters('moodleCommon').httpsTermination, 'AppGw'))]", "type": "Microsoft.Network/loadBalancers", "sku": { "name": "[parameters('moodleCommon').lbSku]", "tier": "[parameters('moodleCommon').lbTier]" }, "apiVersion": "2019-11-01", "dependsOn": [ "[concat('Microsoft.Network/publicIPAddresses/',parameters('moodleCommon').lbPipName)]", "[concat('Microsoft.Network/publicIPAddresses/',parameters('moodleCommon').lbOutPipName001)]", "[concat('Microsoft.Network/publicIPAddresses/',parameters('moodleCommon').lbOutPipName002)]" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').lbName]", "properties": { "backendAddressPools": [ { "name": "[parameters('moodleCommon').extBeName ]" } ], "frontendIPConfigurations": [ { "name": "[parameters('moodleCommon').extFeName ]", "properties": { "publicIPAddress": { "id": "[variables('lbPipID')]" } } }, { "name": "[parameters('moodleCommon').extOutName001 ]", "properties": { "publicIPAddress": { "id": "[variables('lbOutPip001ID')]" } } }, { "name": "[parameters('moodleCommon').extOutName002 ]", "properties": { "publicIPAddress": { "id": "[variables('lbOutPip002ID')]" } } } ], "loadBalancingRules": [ { "name": "HTTP", "properties": { "backendAddressPool": { "id": "[variables('extBeID')]" }, "backendPort": 80, "enableFloatingIP": false, "enableTcpReset": false, "loadDistribution": "Default", "disableOutboundSnat": true, "frontendIPConfiguration": { "id": "[variables('extFeID')]" }, "frontendPort": 80, "idleTimeoutInMinutes": 5, "probe": { "id": "[variables('extProbeHTTPID')]" }, "protocol": "Tcp" } }, { "name": "HTTPS", "properties": { "backendAddressPool": { "id": "[variables('extBeID')]" }, "backendPort": 443, "enableFloatingIP": false, "enableTcpReset": false, "loadDistribution": "Default", "disableOutboundSnat": true, "frontendIPConfiguration": { "id": "[variables('extFeID')]" }, "frontendPort": 443, "idleTimeoutInMinutes": 5, "probe": { "id": "[variables('extProbeHTTPSID')]" }, "protocol": "Tcp" } } ], "probes": [ { "name": "[parameters('moodleCommon').extProbeHTTP ]", "properties": { "intervalInSeconds": 5, "numberOfProbes": 3, "port": 80, "protocol": "Tcp" } }, { "name": "[parameters('moodleCommon').extProbeHTTPS ]", "properties": { "intervalInSeconds": 5, "numberOfProbes": 3, "port": 443, "protocol": "Tcp" } } ], "inboundNatRules": [], "outboundRules": [ { "name": "Outboundrule001", "properties": { "allocatedOutboundPorts": 12800, "protocol": "Tcp", "enableTcpReset": false, "idleTimeoutInMinutes": 5, "backendAddressPool": { "id": "[variables('extBeID')]" }, "frontendIPConfigurations": [ { "id": "[variables('extOutID001')]" }, { "id": "[variables('extOutID002')]" } ] } } ], "inboundNatPools": [] } }, { "condition": "[equals(parameters('moodleCommon').httpsTermination, 'AppGw')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "Microsoft.Resources/deployments/subnetTemplate", "[concat('Microsoft.Network/publicIPAddresses/',parameters('moodleCommon').appGwPipName)]" ], "name": "appGwTemplate", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "subnetIdAppGw": { "value": "[variables('subnetIdAppGw')]" }, "sslCertData": { "reference": { "keyVault": { "id": "[parameters('moodleCommon').appGwSslCertKeyVaultResourceId]" }, "secretName": "[parameters('moodleCommon').appGwSslCertKeyVaultSecretName]" } } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'appgw.json',parameters('moodleCommon').artifactsSasToken)]" } } } ], "variables": { "documentation01": "This sub-template creates a virtual network with a number of subnets and then creates the moodle load-balancer (or an Azure Application Gateway) with public IP/dns", "extBeID": "[concat(variables('extLbID'),'/backendAddressPools/',parameters('moodleCommon').extBeName)]", "extFeID": "[concat(variables('extLbID'),'/frontendIPConfigurations/',parameters('moodleCommon').extFeName)]", "extOutID001": "[concat(variables('extLbID'),'/frontendIPConfigurations/',parameters('moodleCommon').extOutName001)]", "extOutID002": "[concat(variables('extLbID'),'/frontendIPConfigurations/',parameters('moodleCommon').extOutName002)]", "extLbID": "[resourceId('Microsoft.Network/loadBalancers',parameters('moodleCommon').lbName)]", "extProbeHTTPID": "[concat(variables('extLbID'),'/probes/',parameters('moodleCommon').extProbeHTTP)]", "extProbeHTTPSID": "[concat(variables('extLbID'),'/probes/',parameters('moodleCommon').extProbeHTTPS)]", "lbPipID": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('moodleCommon').lbPipName)]", "lbOutPip001ID": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('moodleCommon').lbOutPipName001)]", "lbOutPip002ID": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('moodleCommon').lbOutPipName002)]", "ctlrPipID": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('moodleCommon').ctlrPipName)]", "customVnetIdArr": "[split(parameters('moodleCommon').customVnetId, '/')]", "vnetSub": "[if(equals(parameters('moodleCommon').customVnetId, ''), subscription().subscriptionId, variables('customVnetIdArr')[2])]", "vnetRg": "[if(equals(parameters('moodleCommon').customVnetId, ''), resourceGroup().name, variables('customVnetIdArr')[4])]", "vnetName": "[if(equals(parameters('moodleCommon').customVnetId, ''), parameters('moodleCommon').vnetName, variables('customVnetIdArr')[8])]", "vnetId": "[if(equals(parameters('moodleCommon').customVnetId, ''), resourceId('Microsoft.Network/virtualNetworks', parameters('moodleCommon').vnetName), parameters('moodleCommon').customVnetId)]", "customVnetSubnetIdWeb": "[concat(parameters('moodleCommon').customVnetId, '/subnets/', parameters('moodleCommon').subnetWeb)]", "customVnetSubnetIdSan": "[concat(parameters('moodleCommon').customVnetId, '/subnets/', parameters('moodleCommon').subnetSan)]", "customVnetSubnetIdRedis": "[concat(parameters('moodleCommon').customVnetId, '/subnets/', parameters('moodleCommon').subnetRedis)]", "customVnetSubnetIdElastic": "[concat(parameters('moodleCommon').customVnetId, '/subnets/', parameters('moodleCommon').subnetElastic)]", "customVnetSubnetIdTika": "[concat(parameters('moodleCommon').customVnetId, '/subnets/', parameters('moodleCommon').subnetTika)]", "customVnetSubnetIdGateway": "[concat(parameters('moodleCommon').customVnetId, '/subnets/', parameters('moodleCommon').subnetGateway)]", "customVnetSubnetIdAppGw": "[concat(parameters('moodleCommon').customVnetId, '/subnets/', parameters('moodleCommon').subnetAppGw)]", "customVnetSubnetIdDb": "[concat(parameters('moodleCommon').customVnetId, '/subnets/', parameters('moodleCommon').subnetDb)]", "subnetIdWeb": "[if(equals(parameters('moodleCommon').customVnetId, ''), resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), parameters('moodleCommon').subnetWeb), variables('customVnetSubnetIdWeb'))]", "subnetIdSan": "[if(equals(parameters('moodleCommon').customVnetId, ''), resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), parameters('moodleCommon').subnetSan), variables('customVnetSubnetIdSan'))]", "subnetIdRedis": "[if(equals(parameters('moodleCommon').customVnetId, ''), resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), parameters('moodleCommon').subnetRedis), variables('customVnetSubnetIdRedis'))]", "subnetIdElastic": "[if(equals(parameters('moodleCommon').customVnetId, ''), resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), parameters('moodleCommon').subnetElastic), variables('customVnetSubnetIdElastic'))]", "subnetIdTika": "[if(equals(parameters('moodleCommon').customVnetId, ''), resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), parameters('moodleCommon').subnetTika), variables('customVnetSubnetIdTika'))]", "subnetIdGateway": "[if(equals(parameters('moodleCommon').customVnetId, ''), resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), parameters('moodleCommon').subnetGateway), variables('customVnetSubnetIdGateway'))]", "subnetIdAppGw": "[if(equals(parameters('moodleCommon').customVnetId, ''), resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), parameters('moodleCommon').subnetAppGw), variables('customVnetSubnetIdAppGw'))]", "subnetIdDb": "[if(parameters('moodleCommon').vnetDbDeploySwitch, if(equals(parameters('moodleCommon').customVnetId, ''), resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), parameters('moodleCommon').subnetDb), variables('customVnetSubnetIdDb')), '')]" }, "outputs": { "lbPubIp": { "value": "[if(equals(parameters('moodleCommon').httpsTermination, 'AppGw'), '0.0.0.0', reference(parameters('moodleCommon').lbPipName, '2017-10-01').ipAddress)]", "type": "string" }, "lbOut001PubIp": { "value": "[if(equals(parameters('moodleCommon').httpsTermination, 'AppGw'), '0.0.0.0', reference(parameters('moodleCommon').lbOutPipName001, '2017-10-01').ipAddress)]", "type": "string" }, "lbOut002PubIp": { "value": "[if(equals(parameters('moodleCommon').httpsTermination, 'AppGw'), '0.0.0.0', reference(parameters('moodleCommon').lbOutPipName002, '2017-10-01').ipAddress)]", "type": "string" }, "ctlrPubIp": { "value": "[reference(parameters('moodleCommon').ctlrPipName, '2017-10-01').ipAddress]", "type": "string" }, "ctlrPubIpId": { "value": "[variables('ctlrPipID')]", "type": "string" }, "subnetIdWeb": { "value": "[variables('subnetIdWeb')]", "type": "string" }, "subnetIdSan": { "value": "[variables('subnetIdSan')]", "type": "string" }, "subnetIdRedis": { "value": "[variables('subnetIdRedis')]", "type": "string" }, "subnetIdElastic": { "value": "[variables('subnetIdElastic')]", "type": "string" }, "subnetIdTika": { "value": "[variables('subnetIdTika')]", "type": "string" }, "subnetIdDb": { "value": "[variables('subnetIdDb')]", "type": "string" } } } ================================================ FILE: nested/nfs-ha-vm.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "_artifactsLocation": { "type": "string", "metadata": { "description": "The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated." } }, "_artifactsLocationSasToken": { "type": "securestring", "metadata": { "description": "The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated." }, "defaultValue": "" }, "vmIndex": { "metadata": { "description": "Index of the VM" }, "type": "int" }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "Azure location where this template is to be deployed" } }, "subnetId": { "metadata": { "description": "Azure resource ID of the subnet where this NFS-HA cluster is to be deployed" }, "type": "string" }, "ipAddrs": { "metadata": { "description": "Statically assigned private IP addresses that should be assigned to the two VMs' NICs to be deployed. Must belong to the IP range of the specified subnet" }, "type": "array" }, "nfsClientsIPRange": { "metadata": { "description": "IP range of the allowed NFS clients. E.g., 10.0.0.0/24" }, "type": "string" }, "enableAccelNwSwitch": { "metadata": { "description": "Switch to enable Azure Accelerated Networking (Note: this feature is NOT available for D1-level VM SKU)" }, "type": "bool", "defaultValue": false }, "availSetId": { "metadata": { "description": "Azure resource ID of the availability set where this VM is to be deployed" }, "type": "string" }, "vmSku": { "metadata": { "description": "Azure VM SKU for the NFS HA VMs" }, "type": "string", "defaultValue": "Standard_DS2_v2" }, "adminUserName": { "metadata": { "description": "VM admin user name" }, "type": "string", "defaultValue": "azureadmin" }, "sshPublicKey": { "metadata": { "description": "SSH public key for the admin user" }, "type": "string" }, "osType": { "metadata": { "description": "OS type (offer/publisher/sku/version) info" }, "type": "object" }, "osDiskStorageType": { "defaultValue": "Premium_LRS", "allowedValues": [ "Premium_LRS", "Standard_LRS" ], "metadata": { "description": "Azure storage type for all VMs' OS disks. With htmlLocalCopySwith true, Premium_LRS (SSD) is strongly recommended, as PHP files will be served from OS disks." }, "type": "string" }, "dataDiskCountPerVM": { "metadata": { "description": "Number of data disks per VM. 2 or more disks will be configured as RAID0" }, "defaultValue": 1, "minValue": 1, "maxValue": 8, "type": "int" }, "dataDiskSizeInGB": { "defaultValue": 32, "metadata": { "description": "Size of each disk in an NFS server" }, "type": "int" }, "resourcesUniqueString": { "metadata": { "description": "Unique string of fixed length (e.g., 6) identifying related resources" }, "type": "string", "defaultValue": "[substring(uniqueString(resourceGroup().id, deployment().name), 3, 6)]" }, "lbBeId": { "metadata": { "description": "Azure resource ID of the load balancer backend pool to which this VM's NIC should be added" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2017-10-01", "location": "[parameters('location')]", "name": "[variables('nicName')]", "properties": { "ipConfigurations": [ { "name": "[variables('ipcfgName')]", "properties": { "privateIPAllocationMethod": "Static", "privateIPAddress": "[parameters('ipAddrs')[parameters('vmIndex')]]", "subnet": { "id": "[parameters('subnetId')]" }, "loadBalancerBackendAddressPools": [ { "id": "[parameters('lbBeId')]" } ] } } ], "enableAcceleratedNetworking": "[parameters('enableAccelNwSwitch')]" }, "tags": { "displayName": "[concat('NIC for NFS-HA node', parameters('vmIndex'), ' VM')]" } }, { "type": "Microsoft.Compute/virtualMachines", "apiVersion": "2017-03-30", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" ], "location": "[parameters('location')]", "name": "[variables('vmResourceName')]", "properties": { "availabilitySet": { "id": "[parameters('availSetId')]" }, "hardwareProfile": { "vmSize": "[parameters('vmSku')]" }, "networkProfile": { "networkInterfaces": [ { "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" } ] }, "osProfile": { "adminUsername": "[parameters('adminUserName')]", "computerName": "[variables('vmName')]", "linuxConfiguration": { "disablePasswordAuthentication": true, "ssh": { "publicKeys": [ { "path": "[concat('/home/', parameters('adminUserName'), '/.ssh/authorized_keys')]", "keyData": "[parameters('sshPublicKey')]" } ] } } }, "storageProfile": { "imageReference": "[parameters('osType')]", "osDisk": { "createOption": "FromImage", "managedDisk": { "storageAccountType": "[parameters('osDiskStorageType')]" }, "name": "[concat(variables('vmResourceName'), '_osDisk')]" }, "copy": [ { "name": "dataDisks", "count": "[parameters('dataDiskCountPerVM')]", "input": { "managedDisk": { "storageAccountType": "Premium_LRS" }, "diskSizeGB": "[parameters('dataDiskSizeInGB')]", "lun": "[copyIndex('dataDisks')]", "createOption": "Empty" } } ] } }, "resources": [ { "type": "extensions", "apiVersion": "2017-03-30", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/', variables('vmResourceName'))]" ], "location": "[parameters('location')]", "name": "setup_nfs_ha", "properties": { "publisher": "Microsoft.Azure.Extensions", "settings": { "fileUris": [ "[variables('scriptUri')]", "[variables('commonFunctionsScriptUri')]" ] }, "protectedSettings":{ "commandToExecute": "[variables('cmdExec')]" }, "type": "CustomScript", "typeHandlerVersion": "2.0" }, "tags": { "displayName": "NFS-HA VM setup CustomScript extension" } } ], "tags": { "displayName": "[concat('NFS-HA Virtual Machine ', variables('vmName'))]" } } ], "variables": { "nicName": "[concat('nfs-ha-nic', parameters('vmIndex'), '-', parameters('resourcesUniqueString'))]", "ipCfgName": "[concat('nfs-ha-ipcfg', parameters('vmIndex'))]", "vmResourceName": "[concat('nfs-ha-vm', parameters('vmIndex'), '-', parameters('resourcesUniqueString'))]", "vmName": "[concat('hanode', parameters('vmIndex'), '-', parameters('resourcesUniqueString'))]", "scriptUri": "[concat(parameters('_artifactsLocation'), 'scripts/setup_nfs_ha.sh', parameters('_artifactsLocationSasToken'))]", "commonFunctionsScriptUri": "[concat(parameters('_artifactsLocation'), 'scripts/helper_functions.sh', parameters('_artifactsLocationSasToken'))]", "cmdExec": "[concat('bash -x setup_nfs_ha.sh hanode0-', parameters('resourcesUniqueString'), ' ', parameters('ipAddrs')[0], ' hanode1-', parameters('resourcesUniqueString'), ' ', parameters('ipAddrs')[1], ' ', parameters('nfsClientsIPRange'))]" } } ================================================ FILE: nested/nfs-ha.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "_artifactsLocation": { "type": "string", "metadata": { "description": "The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated." } }, "_artifactsLocationSasToken": { "type": "securestring", "metadata": { "description": "The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated." }, "defaultValue": "" }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "Azure location where this template is to be deployed" } }, "subnetId": { "metadata": { "description": "Azure resource ID of the subnet where this NFS-HA cluster is to be deployed" }, "type": "string" }, "node0IPAddr": { "metadata": { "description": "IP address of node 0 (statically assigned). E.g., 10.0.0.11. Must belong to the IP range of the specified subnet" }, "type": "string" }, "node1IPAddr": { "metadata": { "description": "IP address of node 1 (statically assigned). E.g., 10.0.0.22. Must belong to the IP range of the specified subnet" }, "type": "string" }, "nfsClientsIPRange": { "metadata": { "description": "IP range of the allowed NFS clients. E.g., 10.0.0.0/24" }, "type": "string" }, "lbFrontEndIpAddr": { "metadata": { "description": "IP address of the load balancer front-end (statically assigned). E.g., 10.0.0.100. Must belong to the IP range of the specified subnet" }, "type": "string" }, "enableAccelNwSwitch": { "metadata": { "description": "Switch to enable Azure Accelerated Networking (Note: this feature is NOT available for D1-level VM SKU)" }, "type": "bool", "defaultValue": false }, "vmSku": { "metadata": { "description": "Azure VM SKU for the NFS HA VMs" }, "type": "string", "defaultValue": "Standard_DS2_v2" }, "adminUserName": { "metadata": { "description": "VM admin user name" }, "type": "string", "defaultValue": "azureadmin" }, "sshPublicKey": { "metadata": { "description": "SSH public key for the admin user" }, "type": "string" }, "osType": { "metadata": { "description": "OS type (offer/publisher/sku/version) info" }, "type": "object", "defaultValue": { "offer": "UbuntuServer", "publisher": "Canonical", "sku": "16.04-LTS", "version": "latest" } }, "osDiskStorageType": { "defaultValue": "Premium_LRS", "allowedValues": [ "Premium_LRS", "Standard_LRS" ], "metadata": { "description": "Azure storage type for all VMs' OS disks. With htmlLocalCopySwith true, Premium_LRS (SSD) is strongly recommended, as PHP files will be served from OS disks." }, "type": "string" }, "dataDiskCountPerVM": { "metadata": { "description": "Number of data disks per VM. 2 or more disks will be configured as RAID0" }, "defaultValue": 1, "minValue": 1, "maxValue": 8, "type": "int" }, "dataDiskSizeInGB": { "defaultValue": 32, "metadata": { "description": "Size per disk in an NFS server" }, "type": "int" }, "resourcesUniqueString": { "metadata": { "description": "Unique string of fixed length (e.g., 6) identifying related resources" }, "type": "string", "defaultValue": "[substring(uniqueString(resourceGroup().id, deployment().name), 3, 6)]" } }, "resources": [ { "type": "Microsoft.Compute/availabilitySets", "apiVersion": "2017-03-30", "location": "[parameters('location')]", "name": "[variables('availSetName')]", "properties": { "platformFaultDomainCount": 2, "platformUpdateDomainCount": 5 }, "sku": { "name": "Aligned" }, "tags": { "displayName": "NFS-HA Availability Set" } }, { "type": "Microsoft.Network/loadBalancers", "sku": { "name": "Basic" }, "apiVersion": "2017-10-01", "location": "[parameters('location')]", "name": "[variables('nfsHaLbName')]", "properties": { "frontendIPConfigurations": [ { "name": "[variables('nfsHaLbFeName')]", "properties": { "privateIPAddress": "[parameters('lbFrontEndIpAddr')]", "privateIPAllocationMethod": "Static", "subnet": { "id": "[parameters('subnetId')]" } } } ], "backendAddressPools": [ { "name": "[variables('nfsHaLbBeName')]" } ], "loadBalancingRules": [ { "name": "[concat(variables('nfsHaLbRuleName'), '-nfsd-tcp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2049, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2049, "protocol": "Tcp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-nfsd-udp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2049, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2049, "protocol": "Udp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-rpcbind-tcp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 111, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 111, "protocol": "Tcp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-rpcbind-udp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 111, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 111, "protocol": "Udp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-mountd-tcp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2000, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2000, "protocol": "Tcp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-mountd-udp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2000, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2000, "protocol": "Udp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-statd-tcp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2001, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2001, "protocol": "Tcp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-statd-udp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2001, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2001, "protocol": "Udp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-statd-outgoing-tcp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2002, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2002, "protocol": "Tcp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-statd-outgoing-udp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2002, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2002, "protocol": "Udp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-quotad-tcp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2003, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2003, "protocol": "Tcp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-quotad-udp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2003, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2003, "protocol": "Udp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-lockd-tcp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2004, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2004, "protocol": "Tcp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-lockd-udp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2004, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2004, "protocol": "Udp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-nfs-callback-tcp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2005, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2005, "protocol": "Tcp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } }, { "name": "[concat(variables('nfsHaLbRuleName'), '-nfs-callback-udp')]", "properties": { "frontendIPConfiguration": { "id": "[variables('nfsHaLbFeId')]" }, "frontendPort": 2005, "backendAddressPool": { "id": "[variables('nfsHaLbBeId')]" }, "backendPort": 2005, "protocol": "Udp", "probe": { "id": "[variables('nfsHaLbProbeId')]" }, "enableFloatingIP": false, "idleTimeoutInMinutes": 4 } } ], "probes": [ { "name": "[variables('nfsHaLbProbeName')]", "properties": { "intervalInSeconds": 5, "numberOfProbes": 2, "port": 61000, "protocol": "Tcp" } } ] } }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "copy": { "count": 2, "name": "nfs-ha-vm-loop" }, "dependsOn": [ "[concat('Microsoft.Compute/availabilitySets/', variables('availSetName'))]", "[concat('Microsoft.Network/loadBalancers/', variables('nfsHaLbName'))]" ], "name": "[concat(variables('vmDeploymentNameBase'), copyIndex())]", "properties": { "mode": "Incremental", "parameters": { "_artifactsLocation": { "value": "[parameters('_artifactsLocation')]" }, "_artifactsLocationSasToken": { "value": "[parameters('_artifactsLocationSasToken')]" }, "location": { "value": "[parameters('location')]" }, "vmIndex": { "value": "[copyindex()]" }, "ipAddrs": { "value": "[variables('nodeIpAddrs')]" }, "nfsClientsIPRange": { "value": "[parameters('nfsClientsIPRange')]" }, "subnetId": { "value": "[parameters('subnetId')]" }, "enableAccelNwSwitch": { "value": "[parameters('enableAccelNwSwitch')]" }, "availSetId": { "value": "[resourceId('Microsoft.Compute/AvailabilitySets', variables('availSetName'))]" }, "vmSku": { "value": "[parameters('vmSku')]" }, "adminUserName": { "value": "[parameters('adminUserName')]" }, "sshPublicKey": { "value": "[parameters('sshPublicKey')]" }, "osType": { "value": "[parameters('osType')]" }, "osDiskStorageType": { "value": "[parameters('osDiskStorageType')]" }, "dataDiskCountPerVM": { "value": "[parameters('dataDiskCountPerVM')]" }, "dataDiskSizeInGB": { "value": "[parameters('dataDiskSizeInGB')]" }, "resourcesUniqueString": { "value": "[parameters('resourcesUniqueString')]" }, "lbBeId": { "value": "[variables('nfsHaLbBeId')]" } }, "templateLink": { "uri": "[concat(parameters('_artifactsLocation'), 'nested/nfs-ha-vm.json', parameters('_artifactsLocationSasToken'))]" } } } ], "variables": { "availSetName": "[concat('nfs-ha-availset-', parameters('resourcesUniqueString'))]", "vmDeploymentNameBase": "nfs-ha-vm-deployment", "nodeIpAddrs": [ "[parameters('node0IPAddr')]", "[parameters('node1IPAddr')]" ], "nfsHaLbName": "[concat('nfs-ha-lb-', parameters('resourcesUniqueString'))]", "nfsHaLbFeName": "nfs-ha-lb-fe", "nfsHaLbFeId": "[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', variables('nfsHaLbName'), variables('nfsHaLbFeName'))]", "nfsHaLbBeName": "nfs-ha-lb-be", "nfsHaLbBeId": "[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', variables('nfsHaLbName'), variables('nfsHaLbBeName'))]", "nfsHaLbRuleName": "nfs-ha-lb-rule", "nfsHaLbProbeName": "nfs-ha-lb-probe", "nfsHaLbProbeId": "[resourceId('Microsoft.Network/loadBalancers/probes', variables('nfsHaLbName'), variables('nfsHaLbProbeName'))]" } } ================================================ FILE: nested/recoveryservices.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" } }, "resources": [ { "type": "Microsoft.RecoveryServices/vaults", "apiVersion": "2016-06-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').vaultName]", "properties": {}, "sku": { "name": "RS0", "tier": "Standard" } }, { "type": "Microsoft.RecoveryServices/vaults/backupPolicies", "apiVersion": "2017-07-01", "dependsOn": [ "[concat('Microsoft.RecoveryServices/vaults/', parameters('moodleCommon').vaultName)]" ], "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('moodleCommon').vaultName, '/', parameters('moodleCommon').policyName)]", "properties": { "backupManagementType": "AzureIaasVM", "retentionPolicy": { "dailySchedule": { "retentionDuration": { "count": "[variables( 'dailyRetentionDurationCount')]", "durationType": "Days" }, "retentionTimes": "[variables('scheduleRunTimes')]" }, "monthlySchedule": { "retentionDuration": { "count": "[variables('monthlyRetentionDurationCount')]", "durationType": "Months" }, "retentionScheduleDaily": { "daysOfTheMonth": [ { "date": 1, "isLast": false } ] }, "retentionScheduleFormatType": "Daily", "retentionScheduleWeekly": null, "retentionTimes": "[variables('scheduleRunTimes')]" }, "retentionPolicyType": "LongTermRetentionPolicy", "weeklySchedule": { "daysOfTheWeek": "[variables('daysOfTheWeek')]", "retentionDuration": { "count": "[variables( 'weeklyRetentionDurationCount')]", "durationType": "Weeks" }, "retentionTimes": "[variables('scheduleRunTimes')]" } }, "schedulePolicy": { "schedulePolicyType": "SimpleSchedulePolicy", "scheduleRunDays": null, "scheduleRunFrequency": "Daily", "scheduleRunTimes": "[variables('scheduleRunTimes')]" } } } ], "variables": { "dailyRetentionDurationCount": 7, "daysOfTheWeek": [ "Sunday" ], "documentation1": "This sub-template creates a recovery services vault. It expects certain values in the 'common' datastructure.", "documentation2": " vaultName - name of virtual network", "documentation3": " policyName - name of backup policy inside vault", "documentation4": "", "documentation5": "The policy will create a daily backup with the following retentions", "documentation6": " Daily - keep last 7 daily", "documentation7": " Weekly - keep last 4 Sundays", "documentation8": " Monthly - keep last 6 1st-of-the-month", "monthlyRetentionDurationCount": 6, "scheduleRunTimes": [ "2017-01-01T22:30:00Z" ], "weeklyRetentionDurationCount": 4 } } ================================================ FILE: nested/recoveryservicesEnlist.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "vmName": { "metadata": { "description": "Name of VM to enlist in AzureBackup" }, "type": "string" } }, "resources": [ { "type": "Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems", "apiVersion": "2016-06-01", "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('moodleCommon').vaultName, '/', variables('backupFabric'), '/', variables('v2VmContainer'), concat(resourceGroup().name,';',parameters('vmName')), '/', variables('v2Vm'), concat(resourceGroup().name,';',parameters('vmName')))]", "properties": { "policyId": "[resourceId('Microsoft.RecoveryServices/vaults/backupPolicies',parameters('moodleCommon').vaultName,parameters('moodleCommon').policyName )]", "protectedItemType": "[variables('v2VmType')]", "sourceResourceId": "[resourceId(subscription().subscriptionId,resourceGroup().name,'Microsoft.Compute/virtualMachines',parameters('vmName'))]" } } ], "variables": { "backupFabric": "Azure", "documentation1": "This sub-template adds a VM to the recovery services vault. It expects certain values in the 'common' datastructure.", "documentation2": " vaultName - name of virtual network", "documentation3": " policyName - name of backup policy inside vault", "documentation4": "", "v2Vm": "vm;iaasvmcontainerv2;", "v2VmContainer": "iaasvmcontainer;iaasvmcontainerv2;", "v2VmType": "Microsoft.Compute/virtualMachines" } } ================================================ FILE: nested/redis.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "subnetIdRedis": { "metadata": { "description": "Azure resource ID of the subnet where this Redis instance is to be deployed" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Cache/Redis", "apiVersion": "2016-04-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').redisCacheName]", "properties": { "enableNonSslPort": true, "sku": { "capacity": 1, "family": "P", "name": "Premium" }, "subnetId": "[parameters('subnetIdRedis')]" } } ], "variables": { "documentation1": "This sub-template creates a redis cache. It expects certain values in the 'common' datastructure.", "documentation2": " redisCacheName - name of cache", "redisResourceId": "[resourceId('Microsoft.Cache/Redis', parameters('moodleCommon').redisCacheName)]" }, "outputs": { "redisKey": { "value": "[listKeys(variables('redisResourceId'), '2016-04-01').primaryKey]", "type": "string" } } } ================================================ FILE: nested/search-azure.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "subnetIdElastic": { "metadata": { "description": "Azure resource ID of the subnet where the Elastic Search VMs are to be deployed (if any)" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Search/searchServices", "apiVersion": "2015-08-19", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').azureSearchName]", "sku": { "name": "[parameters('moodleCommon').azureSearchSku]" }, "properties": { "replicaCount": "[parameters('moodleCommon').azureSearchReplicaCount]", "partitionCount": "[parameters('moodleCommon').azureSearchPartitionCount]", "hostingMode": "[parameters('moodleCommon').azureSearchHostingMode]" } } ], "variables": { "documentation01": "This sub-template drives the Azure Search which is used as the access-point for other moodle VM's ", "documentation02": "It expects certain values in the 'common' datastructure.", "documentation03": " azureSearchName - the name of the Azure Search.", "documentation04": " azureSearchSku - the level of Azure Search service.", "documentation06": " azureSearchReplicaCount - number of Azure Search replicas.", "documentation07": " azureSearchPartitionCount - number of Azure Search partitions.", "documentation08": " azureSearchHostingMode - the type of Azure Search hosting mode.", "azureSearchServiceId": "[resourceId('Microsoft.Search/searchServices', parameters('moodleCommon').azureSearchName)]" }, "outputs": { "azureSearchKey": { "value": "[listAdminKeys(variables('azureSearchServiceId'), '2015-08-19').PrimaryKey]", "type": "string" } } } ================================================ FILE: nested/search-elastic-config.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" } }, "resources": [ { "type": "Microsoft.Compute/virtualMachines/extensions", "apiVersion": "2017-03-30", "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('moodleCommon').elasticVmName1,'/','install_elastic')]", "properties": { "autoUpgradeMinorVersion": true, "publisher": "Microsoft.Azure.Extensions", "settings": { "fileUris": [ "[variables('scriptUri')]" ] }, "protectedSettings":{ "commandToExecute": "[variables('cmdExec')]" }, "type": "CustomScript", "typeHandlerVersion": "2.0" }, "tags": { "displayName": "install_elastic" } }, { "type": "Microsoft.Compute/virtualMachines/extensions", "apiVersion": "2017-03-30", "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('moodleCommon').elasticVmName2,'/','install_elastic')]", "properties": { "autoUpgradeMinorVersion": true, "publisher": "Microsoft.Azure.Extensions", "settings": { "fileUris": [ "[variables('scriptUri')]" ] }, "protectedSettings":{ "commandToExecute": "[variables('cmdExec')]" }, "type": "CustomScript", "typeHandlerVersion": "2.0" }, "tags": { "displayName": "install_elastic" } }, { "type": "Microsoft.Compute/virtualMachines/extensions", "apiVersion": "2017-03-30", "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('moodleCommon').elasticVmName3,'/','install_elastic')]", "properties": { "autoUpgradeMinorVersion": true, "publisher": "Microsoft.Azure.Extensions", "settings": { "fileUris": [ "[variables('scriptUri')]" ] }, "protectedSettings":{ "commandToExecute": "[variables('cmdExec')]" }, "type": "CustomScript", "typeHandlerVersion": "2.0" }, "tags": { "displayName": "install_elastic" } } ], "variables": { "cmdExec": "[concat('bash ', parameters('moodleCommon').elasticScriptFilename, ' ', parameters('moodleCommon').elasticClusterName, ' ', parameters('moodleCommon').elasticVm1IP, ' ', parameters('moodleCommon').elasticVm2IP, ' ', parameters('moodleCommon').elasticVm3IP)]", "documentation01": "This sub-template applies a specific post-deployment script to the controller vm", "documentation02": "It expects certain values in the 'common' datastructure.", "documentation03": " scriptLocation - web URI", "documentation04": " elasticScriptFilename - name of script file", "documentation05": " elasticVmName - name of the elastic search vm generic name", "scriptUri": "[concat(parameters('moodleCommon').scriptLocation,parameters('moodleCommon').elasticScriptFilename,parameters('moodleCommon').artifactsSasToken)]" } } ================================================ FILE: nested/search-elastic.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "subnetIdElastic": { "metadata": { "description": "Azure resource ID of the subnet where the Elastic Search VMs are to be deployed (if any)" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2017-10-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').elasticNicName1]", "properties": { "ipConfigurations": [ { "name": "ipcfg-elastic1", "properties": { "privateIPAllocationMethod": "Static", "privateIPAddress": "[parameters('moodleCommon').elasticVm1IP]", "subnet": { "id": "[parameters('subnetIdElastic')]" } } } ], "enableAcceleratedNetworking": "[parameters('moodleCommon').enableAccelNwForOtherVmsSwitch]" }, "tags": { "displayName": "Elastic NIC 1" } }, { "type": "Microsoft.Compute/virtualMachines", "apiVersion": "2017-03-30", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', parameters('moodleCommon').elasticNicName1)]" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').elasticVmName1]", "properties": { "hardwareProfile": { "vmSize": "[parameters('moodleCommon').elasticVmSku]" }, "networkProfile": { "networkInterfaces": [ { "id": "[variables('nicRef1')]" } ] }, "osProfile": { "adminUsername": "[parameters('moodleCommon').sshUsername]", "computerName": "[parameters('moodleCommon').elasticVmName1]", "linuxConfiguration": { "disablePasswordAuthentication": true, "ssh": { "publicKeys": [ { "path": "[concat('/home/', parameters('moodleCommon').sshUsername, '/.ssh/authorized_keys')]", "keyData": "[parameters('moodleCommon').sshPublicKey]" } ] } } }, "storageProfile": { "dataDisks": [], "imageReference": "[parameters('moodleCommon').osType]", "osDisk": { "createOption": "FromImage", "managedDisk": { "storageAccountType": "[parameters('moodleCommon').osDiskStorageType]" }, "name": "[parameters('moodleCommon').elasticVmName1]" } } }, "tags": { "displayName": "Elastic Search Virtual Machine" } }, { "condition": "[parameters('moodleCommon').applyScriptsSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/', parameters('moodleCommon').elasticVmName1)]" ], "name": "[concat(parameters('moodleCommon').elasticVmName1,'-ScriptProcessor')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'search-elastic-config.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "condition": "[parameters('moodleCommon').azureBackupSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/',parameters('moodleCommon').elasticVmName1)]" ], "name": "[concat(parameters('moodleCommon').elasticVmName1,'-Backup')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vmName": { "value": "[parameters('moodleCommon').elasticVmName1]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'recoveryservicesEnlist.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2017-10-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').elasticNicName2]", "properties": { "ipConfigurations": [ { "name": "ipcfg-elastic2", "properties": { "privateIPAllocationMethod": "Static", "privateIPAddress": "[parameters('moodleCommon').elasticVm2IP]", "subnet": { "id": "[parameters('subnetIdElastic')]" } } } ], "enableAcceleratedNetworking": "[parameters('moodleCommon').enableAccelNwForOtherVmsSwitch]" }, "tags": { "displayName": "Elastic NIC 2" } }, { "type": "Microsoft.Compute/virtualMachines", "apiVersion": "2017-03-30", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', parameters('moodleCommon').elasticNicName2)]" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').elasticVmName2]", "properties": { "hardwareProfile": { "vmSize": "[parameters('moodleCommon').elasticVmSku]" }, "networkProfile": { "networkInterfaces": [ { "id": "[variables('nicRef2')]" } ] }, "osProfile": { "adminUsername": "[parameters('moodleCommon').sshUsername]", "computerName": "[parameters('moodleCommon').elasticVmName2]", "linuxConfiguration": { "disablePasswordAuthentication": true, "ssh": { "publicKeys": [ { "path": "[concat('/home/', parameters('moodleCommon').sshUsername, '/.ssh/authorized_keys')]", "keyData": "[parameters('moodleCommon').sshPublicKey]" } ] } } }, "storageProfile": { "dataDisks": [], "imageReference": "[parameters('moodleCommon').osType]", "osDisk": { "createOption": "FromImage", "managedDisk": { "storageAccountType": "[parameters('moodleCommon').osDiskStorageType]" }, "name": "[parameters('moodleCommon').elasticVmName2]" } } }, "tags": { "displayName": "Elastic Search Virtual Machine" } }, { "condition": "[parameters('moodleCommon').applyScriptsSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/', parameters('moodleCommon').elasticVmName2)]" ], "name": "[concat(parameters('moodleCommon').elasticVmName2,'-ScriptProcessor')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'search-elastic-config.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "condition": "[parameters('moodleCommon').azureBackupSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/',parameters('moodleCommon').elasticVmName2)]" ], "name": "[concat(parameters('moodleCommon').elasticVmName2,'-Backup')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vmName": { "value": "[parameters('moodleCommon').elasticVmName2]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'recoveryservicesEnlist.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2017-10-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').elasticNicName3]", "properties": { "ipConfigurations": [ { "name": "ipcfg-elastic3", "properties": { "privateIPAllocationMethod": "Static", "privateIPAddress": "[parameters('moodleCommon').elasticVm3IP]", "subnet": { "id": "[parameters('subnetIdElastic')]" } } } ], "enableAcceleratedNetworking": "[parameters('moodleCommon').enableAccelNwForOtherVmsSwitch]" }, "tags": { "displayName": "Elastic NIC 2" } }, { "type": "Microsoft.Compute/virtualMachines", "apiVersion": "2017-03-30", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', parameters('moodleCommon').elasticNicName3)]" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').elasticVmName3]", "properties": { "hardwareProfile": { "vmSize": "[parameters('moodleCommon').elasticVmSku]" }, "networkProfile": { "networkInterfaces": [ { "id": "[variables('nicRef3')]" } ] }, "osProfile": { "adminUsername": "[parameters('moodleCommon').sshUsername]", "computerName": "[parameters('moodleCommon').elasticVmName3]", "linuxConfiguration": { "disablePasswordAuthentication": true, "ssh": { "publicKeys": [ { "path": "[concat('/home/', parameters('moodleCommon').sshUsername, '/.ssh/authorized_keys')]", "keyData": "[parameters('moodleCommon').sshPublicKey]" } ] } } }, "storageProfile": { "dataDisks": [], "imageReference": "[parameters('moodleCommon').osType]", "osDisk": { "createOption": "FromImage", "managedDisk": { "storageAccountType": "[parameters('moodleCommon').osDiskStorageType]" }, "name": "[parameters('moodleCommon').elasticVmName3]" } } }, "tags": { "displayName": "Elastic Search Virtual Machine" } }, { "condition": "[parameters('moodleCommon').applyScriptsSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/', parameters('moodleCommon').elasticVmName3)]" ], "name": "[concat(parameters('moodleCommon').elasticVmName3,'-ScriptProcessor')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'search-elastic-config.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "condition": "[parameters('moodleCommon').azureBackupSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/',parameters('moodleCommon').elasticVmName3)]" ], "name": "[concat(parameters('moodleCommon').elasticVmName3,'-Backup')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vmName": { "value": "[parameters('moodleCommon').elasticVmName3]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'recoveryservicesEnlist.json',parameters('moodleCommon').artifactsSasToken)]" } } } ], "variables": { "documentation01": "This sub-template drives the elastic which is used as the access-point for other moodle VM's ", "documentation02": "It expects certain values in the 'common' datastructure.", "documentation03": " vnetName - name of the virtual network", "documentation04": " subnetElastic - name of subnet for elastic (and vm scale set)", "documentation06": " elasticNicName1 - name of the eastlic vm 1 network interface", "documentation07": " elasticNicName2 - name of the eastlic vm 2 network interface", "documentation08": " elasticNicName3 - name of the eastlic vm 3 network interface", "documentation09": " elasticVmName1 - name of the eastlic vm 1", "documentation10": " elasticVmName2 - name of the eastlic vm 2", "documentation11": " elasticVmName3 - name of the eastlic vm 3", "documentation12": " elasticVm1IP - IP of the eastlic vm 1", "documentation13": " elasticVm2IP - IP of the eastlic vm 2", "documentation14": " elasticVm3IP - IP of the eastlic vm 3", "documentation15": "This sub-template calls other sub-templates", "documentation16": " elasticconfig - conditionally applies post-deployment script on the VM", "documentation17": " recoveryservicesEnlist - conditionally enlists the VM into the backup regimen", "nicRef1": "[resourceId('Microsoft.Network/networkInterfaces', parameters('moodleCommon').elasticNicName1)]", "nicRef2": "[resourceId('Microsoft.Network/networkInterfaces', parameters('moodleCommon').elasticNicName2)]", "nicRef3": "[resourceId('Microsoft.Network/networkInterfaces', parameters('moodleCommon').elasticNicName3)]" } } ================================================ FILE: nested/storageAccount.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" } }, "resources": [ { "condition": "[not(equals(parameters('moodleCommon').fileServerType, 'azurefiles'))]", "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2019-06-01", "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('moodleCommon').storageAccountName,'naf')]", "kind": "Storage", "sku": { "name": "[parameters('moodleCommon').storageAccountType]" }, "properties": { "encryption": { "keySource": "Microsoft.Storage", "services": { "blob": { "enabled": true }, "file": { "enabled": true } } }, "networkAcls": { "bypass": "AzureServices", "defaultAction": "Allow", "ipRules": [], "virtualNetworkRules": [] }, "supportsHttpsTrafficOnly": true } }, { "condition": "[equals(parameters('moodleCommon').fileServerType, 'azurefiles')]", "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2019-06-01", "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('moodleCommon').storageAccountName,'af')]", "kind": "[if(equals(parameters('moodleCommon').storageAccountType, 'Premium_LRS'), 'FileStorage', 'Storage')]", "sku": { "name": "[parameters('moodleCommon').storageAccountType]" }, "properties": { "largeFileSharesState": "Enabled", "networkAcls": { "bypass": "AzureServices", "virtualNetworkRules": [], "ipRules": [], "defaultAction": "Allow" }, "supportsHttpsTrafficOnly": true, "encryption": { "services": { "file": { "keyType": "Account", "enabled": true }, "blob": { "keyType": "Account", "enabled": true } }, "keySource": "Microsoft.Storage" } } } ], "variables": { "documentation1": "This sub-template creates a storage account. It expects certain values in the 'common' datastructure.", "documentation2": " storageAccountName - name of storage account", "documentation3": " storageAccountType - type of storage account", "storageName": "[concat(parameters('moodleCommon').storageAccountName,if(equals(parameters('moodleCommon').fileServerType, 'azurefiles'), 'af', 'naf'))]", "storageAccountId": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]" }, "outputs": { "storageAccountKey": { "value": "[listKeys(variables('storageAccountId'), '2019-06-01').keys[0].value]", "type": "string" }, "storageAccountName": { "value": "[variables('storageName')]", "type": "string" } } } ================================================ FILE: nested/tika.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "subnetIdTika": { "metadata": { "description": "Azure resource ID of the subnet where this Tika VM is to be deployed" }, "type": "string" } }, "resources": [ { "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2017-10-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').tikaNicName]", "properties": { "ipConfigurations": [ { "name": "ipcfg-tika", "properties": { "privateIPAllocationMethod": "Static", "privateIPAddress": "[parameters('moodleCommon').tikaVmIP]", "subnet": { "id": "[parameters('subnetIdTika')]" } } } ], "enableAcceleratedNetworking": "[parameters('moodleCommon').enableAccelNwForOtherVmsSwitch]" }, "tags": { "displayName": "Tika NIC" } }, { "type": "Microsoft.Compute/virtualMachines", "apiVersion": "2017-03-30", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', parameters('moodleCommon').tikaNicName)]" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').tikaVmName]", "properties": { "hardwareProfile": { "vmSize": "[parameters('moodleCommon').tikaVmSku]" }, "networkProfile": { "networkInterfaces": [ { "id": "[variables('nicRef')]" } ] }, "osProfile": { "adminUsername": "[parameters('moodleCommon').sshUsername]", "computerName": "[parameters('moodleCommon').tikaVmName]", "linuxConfiguration": { "disablePasswordAuthentication": true, "ssh": { "publicKeys": [ { "path": "[concat('/home/', parameters('moodleCommon').sshUsername, '/.ssh/authorized_keys')]", "keyData": "[parameters('moodleCommon').sshPublicKey]" } ] } } }, "storageProfile": { "dataDisks": [], "imageReference": "[parameters('moodleCommon').osType]", "osDisk": { "createOption": "FromImage", "managedDisk": { "storageAccountType": "[parameters('moodleCommon').osDiskStorageType]" }, "name": "[parameters('moodleCommon').tikaVmName]" } } }, "tags": { "displayName": "Tika Service Virtual Machine" } }, { "condition": "[parameters('moodleCommon').applyScriptsSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/', parameters('moodleCommon').tikaVmName)]" ], "name": "[concat(parameters('moodleCommon').tikaVmName,'-ScriptProcessor')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'tikaconfig.json',parameters('moodleCommon').artifactsSasToken)]" } } }, { "condition": "[parameters('moodleCommon').azureBackupSwitch]", "type": "Microsoft.Resources/deployments", "apiVersion": "2017-05-10", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/',parameters('moodleCommon').tikaVmName)]" ], "name": "[concat(parameters('moodleCommon').tikaVmName,'-Backup')]", "properties": { "mode": "Incremental", "parameters": { "moodleCommon": { "value": "[parameters('moodleCommon')]" }, "vmName": { "value": "[parameters('moodleCommon').tikaVmName]" } }, "templateLink": { "uri": "[concat(parameters('moodleCommon').baseTemplateUrl,'recoveryservicesEnlist.json',parameters('moodleCommon').artifactsSasToken)]" } } } ], "variables": { "documentation01": "This sub-template drives the tika service which is used as the access-point for moodle VM's when using tika search or azure search", "documentation02": "It expects certain values in the 'common' datastructure.", "documentation03": " vnetName - name of the virtual network", "documentation04": " subnetTika - name of subnet for tika (and vm scale set)", "documentation06": " tikaNicName - name of the tika vm network interface", "documentation11": " tikaVmName - name of the tika vm", "documentation12": " tikaVmIP - IP of the tika vm", "documentation15": "This sub-template calls other sub-templates", "documentation16": " tikaconfig - conditionally applies post-deployment script on the VM", "documentation17": " recoveryservicesEnlist - conditionally enlists the VM into the backup regimen", "nicRef": "[resourceId('Microsoft.Network/networkInterfaces', parameters('moodleCommon').tikaNicName)]" } } ================================================ FILE: nested/tikaconfig.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" } }, "resources": [ { "type": "Microsoft.Compute/virtualMachines/extensions", "apiVersion": "2017-03-30", "location": "[parameters('moodleCommon').location]", "name": "[concat(parameters('moodleCommon').tikaVmName,'/','install_tika')]", "properties": { "autoUpgradeMinorVersion": true, "publisher": "Microsoft.Azure.Extensions", "settings": { "fileUris": [ "[variables('scriptUri')]" ] }, "protectedSettings":{ "commandToExecute": "[variables('cmdExec')]" }, "type": "CustomScript", "typeHandlerVersion": "2.0" }, "tags": { "displayName": "install_tika" } } ], "variables": { "cmdExec": "[concat('bash ', parameters('moodleCommon').tikaScriptFilename, ' ', parameters('moodleCommon').tikaVmIP)]", "documentation01": "This sub-template applies a specific post-deployment script to the tika vm", "documentation02": "It expects certain values in the 'common' datastructure.", "documentation03": " scriptLocation - web URI", "documentation04": " tikaScriptFilename - name of script file", "documentation05": " tikaVmName - name of the tika search vm generic name", "scriptUri": "[concat(parameters('moodleCommon').scriptLocation,parameters('moodleCommon').tikaScriptFilename,parameters('moodleCommon').artifactsSasToken)]" } } ================================================ FILE: nested/vmsetupparams.json ================================================ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "dbFQDN": { "metadata": { "description": "FQDN of the deployed SQL DB" }, "type": "string" }, "storageAccountName": { "metadata": { "description": "Storage account name from the storage account deployment." }, "type": "string" }, "storageAccountKey": { "metadata": { "description": "Storage account key from the storage account deployment." }, "type": "string" }, "redisKey": { "metadata": { "description": "Redis cache key from the redis deployment ('None' if redis is not selected). This just gets passed through to the controllersetup.json." }, "type": "string" }, "azureSearchKey": { "metadata": { "description": "Azure Search primary key from the Azure Search service deployment ('None' if Azure Search service is not selected)" }, "type": "string" } }, "resources": [], "variables": { "documentation01": "This sub-template doesn't create any Azure resource, but just constructs/returns a JSON object that'll be injected to controller & VMSS VMs (through cloud-init) so that VM setup custom script can read/use, instead of receiving these as a long list of cmdline args", "vmSetupParamsObj": { "siteProfile": { "siteURL": "[parameters('moodleCommon').siteURL]", "httpsTermination": "[parameters('moodleCommon').httpsTermination]", "thumbprintSslCert": "[parameters('moodleCommon').thumbprintSslCert]", "thumbprintCaCert": "[parameters('moodleCommon').thumbprintCaCert]" }, "moodleProfile": { "version": "[parameters('moodleCommon').moodleVersion]", "dbName": "[parameters('moodleCommon').moodleDbName]", "dbUser": "[parameters('moodleCommon').moodleDbUser]", "dbUserAzure": "[parameters('moodleCommon').moodleDbUserAzure]", "dbPassword": "[parameters('moodleCommon').moodleDbPass]", "adminPassword": "[parameters('moodleCommon').moodleAdminPass]", "storageAccountName": "[parameters('storageAccountName')]", "storageAccountKey": "[parameters('storageAccountKey')]", "storageAccountType": "[parameters('moodleCommon').storageAccountType]", "redisDns": "[parameters('moodleCommon').redisDns]", "redisKey": "[parameters('redisKey')]", "elasticVm1IP": "[parameters('moodleCommon').elasticVm1IP]", "installO365pluginsSwitch": "[parameters('moodleCommon').installO365pluginsSwitch]", "installObjectFsSwitch": "[parameters('moodleCommon').installObjectFsSwitch]", "installGdprPluginsSwitch": "[parameters('moodleCommon').installGdprPluginsSwitch]", "searchType": "[parameters('moodleCommon').searchType]", "azureSearchKey": "[parameters('azureSearchKey')]", "azureSearchNameHost": "[parameters('moodleCommon').azureSearchNameHost]", "tikaVmIP": "[parameters('moodleCommon').tikaVmIP]", "syslogServer": "[parameters('moodleCommon').ctlrVmName]", "webServerType": "[parameters('moodleCommon').webServerType]", "htmlLocalCopySwitch": "[parameters('moodleCommon').htmlLocalCopySwitch]", "isMigration": "[parameters('moodleCommon').isMigration]" }, "dbServerProfile": { "type": "[if(equals(parameters('moodleCommon').dbServerType, 'mysqlflex'), 'mysql', parameters('moodleCommon').dbServerType)]", "fqdn": "[parameters('dbFQDN')]", "adminLogin": "[parameters('moodleCommon').dbLogin]", "adminLoginAzure": "[if(equals(parameters('moodleCommon').dbServerType, 'mysqlflex'), parameters('moodleCommon').dbLogin, concat(parameters('moodleCommon').dbLogin, '@', parameters('moodleCommon').dbServerType, '-', parameters('moodleCommon').resourcesPrefix))]", "adminPassword": "[parameters('moodleCommon').dbLoginPassword]", "mssqlDbServiceObjectiveName": "[parameters('moodleCommon').mssqlDbServiceObjectiveName]", "mssqlDbEdition": "[parameters('moodleCommon').mssqlDbEdition]", "mssqlDbSize": "[parameters('moodleCommon').mssqlDbSize]" }, "fileServerProfile": { "type": "[parameters('moodleCommon').fileServerType]", "nfsVmName": "[parameters('moodleCommon').ctlrVmName]", "glusterVmName": "[concat(parameters('moodleCommon').gfsNameRoot, '0')]", "glusterVolName": "data", "nfsByoIpExportPath": "[parameters('moodleCommon').nfsByoIpExportPath]", "nfsHaLbIP": "[parameters('moodleCommon').nfsHaLbIP]", "nfsHaExportPath": "[parameters('moodleCommon').nfsHaExportPath]", "fileServerDiskSize": "[parameters('moodleCommon').fileServerDiskSize]" }, "phpProfile": { "phpVersion": "[parameters('moodleCommon').phpVersion]" } } }, "outputs": { "vmSetupParamsObj": { "value": "[variables('vmSetupParamsObj')]", "type": "object" } } } ================================================ FILE: nested/webvmss.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "moodleCommon": { "metadata": { "description": "Common Moodle values" }, "type": "object" }, "subnetIdWeb": { "metadata": { "description": "Azure resource ID of the subnet where this VMSS is to be deployed" }, "type": "string" }, "vmSetupParamsObj": { "metadata": { "description": "JSON-structured VM setup params that'll be injected to the VM (through cloud-init) and used by the custom script (setup_webserver.sh)" }, "type": "object" } }, "resources": [ { "type": "Microsoft.Network/networkSecurityGroups", "apiVersion": "2017-10-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').vmssNsgName]", "properties": { "securityRules": [ { "name": "Allow_http", "properties": { "access": "Allow", "destinationAddressPrefix": "*", "destinationPortRange": "80", "direction": "Inbound", "priority": 1000, "protocol": "Tcp", "sourceAddressPrefix": "*", "sourcePortRange": "*" } }, { "name": "Allow_https", "properties": { "access": "Allow", "destinationAddressPrefix": "*", "destinationPortRange": "443", "direction": "Inbound", "priority": 1005, "protocol": "Tcp", "sourceAddressPrefix": "*", "sourcePortRange": "*" } } ] }, "tags": { "displayName": "VMSS NSG" } }, { "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2019-06-01", "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').vmssdStorageAccounttName]", "kind": "Storage", "sku": { "name": "Standard_LRS" } }, { "type": "Microsoft.Compute/virtualMachineScaleSets", "apiVersion": "2019-07-01", "dependsOn": [ "[concat('Microsoft.Storage/storageAccounts/', parameters('moodleCommon').vmssdStorageAccounttName)]", "[concat('Microsoft.Network/networkSecurityGroups/', parameters('moodleCommon').vmssNsgName)]" ], "location": "[parameters('moodleCommon').location]", "name": "[parameters('moodleCommon').vmssName]", "properties": { "overprovision": true, "upgradePolicy": { "mode": "Manual" }, "virtualMachineProfile": { "extensionProfile": { "extensions": [ { "name": "setup_moodle", "properties": { "autoUpgradeMinorVersion": true, "publisher": "Microsoft.Azure.Extensions", "settings": { "fileUris": [ "[variables('scriptUri')]", "[parameters('moodleCommon').commonFunctionsScriptUri]" ] }, "protectedSettings": { "commandToExecute": "[concat('bash ', parameters('moodleCommon').webServerSetupScriptFilename, ' ', parameters('moodleCommon').moodleOnAzureConfigsJsonPath)]" }, "type": "CustomScript", "typeHandlerVersion": "2.0" } } ] }, "networkProfile": { "networkInterfaceConfigurations": [ { "name": "vmssnic", "properties": { "ipConfigurations": [ { "name": "ipcfg_lb", "properties": { "loadBalancerBackendAddressPools": "[take(variables('lbBePoolArray'), variables('lbBePoolArrayTakeCount'))]", "applicationGatewayBackendAddressPools": "[take(variables('appGwBePoolArray'), variables('appGwBePoolArrayTakeCount'))]", "subnet": { "id": "[parameters('subnetIdWeb')]" } } } ], "primary": true, "enableAcceleratedNetworking": "[parameters('moodleCommon').enableAccelNwForOtherVmsSwitch]", "networkSecurityGroup": { "id": "[ variables('vmssNsgNameId') ]" } } } ] }, "osProfile": { "adminUsername": "[parameters('moodleCommon').sshUsername]", "computerNamePrefix": "[parameters('moodleCommon').vmssName]", "customData": "[base64(concat('#cloud-config\nwrite_files:\n- encoding: b64\n content: ', base64(string(parameters('vmSetupParamsObj'))), '\n owner: root:root\n path: ', parameters('moodleCommon').moodleOnAzureConfigsJsonPath, '\n permissions: ', variables('singleQuote'), '0400', variables('singleQuote')))]", "linuxConfiguration": { "disablePasswordAuthentication": true, "ssh": { "publicKeys": [ { "path": "[concat('/home/', parameters('moodleCommon').sshUsername, '/.ssh/authorized_keys')]", "keyData": "[parameters('moodleCommon').sshPublicKey]" } ] } } }, "storageProfile": { "imageReference": "[parameters('moodleCommon').osType]", "osDisk": { "caching": "ReadOnly", "createOption": "FromImage", "diskSizeGB": "[parameters('moodleCommon').OSDiskSizeInGB]", "managedDisk": { "storageAccountType": "[parameters('moodleCommon').osDiskStorageType]" } } } } }, "sku": { "capacity": 1, "name": "[parameters('moodleCommon').autoscaleVmSku]", "tier": "Standard" }, "tags": { "displayName": "webfarm" } }, { "type": "Microsoft.Insights/autoscaleSettings", "apiVersion": "2015-04-01", "dependsOn": [ "[concat('Microsoft.Compute/virtualMachineScaleSets/', parameters('moodleCommon').vmssName)]" ], "location": "[parameters('moodleCommon').location]", "name": "autoscalewad", "properties": { "enabled": true, "name": "autoscalewad", "profiles": [ { "capacity": { "default": "[parameters('moodleCommon').autoscaleVmCountMin]", "maximum": "[parameters('moodleCommon').autoscaleVmCountMax]", "minimum": "[parameters('moodleCommon').autoscaleVmCountMin]" }, "name": "Profile1", "rules": [ { "metricTrigger": { "metricName": "Percentage CPU", "metricNamespace": "", "metricResourceUri": "[variables('vmssID')]", "operator": "GreaterThan", "statistic": "Average", "threshold": 25, "timeAggregation": "Average", "timeGrain": "PT1M", "timeWindow": "PT5M" }, "scaleAction": { "cooldown": "PT30M", "direction": "Increase", "type": "ChangeCount", "value": "2" } }, { "metricTrigger": { "metricName": "Percentage CPU", "metricNamespace": "", "metricResourceUri": "[variables('vmssID')]", "operator": "LessThan", "statistic": "Average", "threshold": 30, "timeAggregation": "Average", "timeGrain": "PT1M", "timeWindow": "PT5M" }, "scaleAction": { "cooldown": "PT20M", "direction": "Decrease", "type": "ChangeCount", "value": "1" } } ] } ], "targetResourceUri": "[variables('vmssID')]" } } ], "variables": { "singleQuote": "'", "dstorID": "[resourceId('Microsoft.Storage/storageAccounts',parameters('moodleCommon').vmssdStorageAccounttName)]", "extBeID": "[concat(variables('extLbID'),'/backendAddressPools/',parameters('moodleCommon').extBeName)]", "extFeID": "[concat(variables('extLbID'),'/frontendIPConfigurations/',parameters('moodleCommon').extFeName)]", "extLbID": "[resourceId('Microsoft.Network/loadBalancers',parameters('moodleCommon').lbName)]", "pipID": "[resourceId('Microsoft.Network/publicIPAddresses',parameters('moodleCommon').lbPipName)]", "scriptUri": "[concat(parameters('moodleCommon').scriptLocation,parameters('moodleCommon').webServerSetupScriptFilename,parameters('moodleCommon').artifactsSasToken)]", "vmssID": "[resourceId('Microsoft.Compute/virtualMachineScaleSets',parameters('moodleCommon').vmssName)]", "webvmss1NIC": "[concat('Microsoft.Compute/virtualMachineScaleSets/', parameters('moodleCommon').vmssName, '/virtualMachines/0/networkInterfaces/vmssnic')]", "appGwID": "[resourceId('Microsoft.Network/applicationGateways', parameters('moodleCommon').appGwName)]", "appGwBePoolId": "[concat(variables('appGwID'), '/backendAddressPools/', parameters('moodleCommon').appGwBePoolName)]", "vmssNsgNameId": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('moodleCommon').vmssNsgName)]", "lbBePoolArray": [ { "id": "[variables('extBeID')]" } ], "lbBePoolArrayTakeCount": "[if(equals(parameters('moodleCommon').httpsTermination, 'AppGw'), 0, 1)]", "appGwBePoolArray": [ { "id": "[variables('appGwBePoolId')]" } ], "appGwBePoolArrayTakeCount": "[if(equals(parameters('moodleCommon').httpsTermination, 'AppGw'), 1, 0)]" }, "outputs": { "webvm1IP": { "value": "[reference(variables('webvmss1NIC'), '2017-03-30').ipConfigurations[0].properties.privateIPAddress]", "type": "string" } } } ================================================ FILE: package.json ================================================ { "name": "azure-moodle", "version": "1.0.0", "description": "A package file for developer depenedencies when testing templates", "scripts": { "test": "grunt test" }, "repository": { "type": "git", "url": "git+https://github.com/Azure/Moodle.git" }, "author": "Catalyst IT", "license": "GPL V3", "bugs": { "url": "https://github.com/Azure/Moodle/issues" }, "homepage": "https://github.com/Azure/Moodle#readme", "devDependencies": { "grunt": ">=1.3.0", "grunt-contrib-jshint": "^0.11.3", "load-grunt-tasks": "^3.3.0" } } ================================================ FILE: scripts/helper_functions.sh ================================================ #!/bin/bash # Common functions definitions function wait_for_process { until [ -z $(/usr/bin/pgrep ${1}) ]; do printf '.' sleep 0.5 done } function apt_update_noninteractive { export DEBIAN_FRONTEND='noninteractive' # waiting for apt to finish before running any other commands wait_for_process apt; apt --yes -qq -o=Dpkg::Use-Pty=0 update } function apt_install_noninteractive { export DEBIAN_FRONTEND='noninteractive' export NEEDRESTART_MODE='a' export ACCEPT_EULA='Y' # waiting for apt to finish before running any other commands wait_for_process apt; apt --yes --no-install-recommends -qq -o=Dpkg::Use-Pty=0 -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install "${@}" } function get_setup_params_from_configs_json { local configs_json_path=${1} # E.g., /var/lib/cloud/instance/moodle_on_azure_configs.json # Added wget command to download jq. wget https://github.com/jqlang/jq/releases/download/jq-1.6/jq-linux64 -O /usr/bin/jq && chmod +x /usr/bin/jq # Wait for the cloud-init write-files user data file to be generated (just in case) local wait_time_sec=0 while [ ! -f "$configs_json_path" ]; do sleep 15 let "wait_time_sec += 15" if [ "$wait_time_sec" -ge "1800" ]; then echo "Error: Cloud-init write-files didn't complete in 30 minutes!" return 1 fi done local json=$(cat $configs_json_path) export moodleVersion=$(echo $json | jq -r .moodleProfile.version) export glusterNode=$(echo $json | jq -r .fileServerProfile.glusterVmName) export glusterVolume=$(echo $json | jq -r .fileServerProfile.glusterVolName) export siteFQDN=$(echo $json | jq -r .siteProfile.siteURL) export httpsTermination=$(echo $json | jq -r .siteProfile.httpsTermination) export dbIP=$(echo $json | jq -r .dbServerProfile.fqdn) export moodledbname=$(echo $json | jq -r .moodleProfile.dbName) export moodledbuser=$(echo $json | jq -r .moodleProfile.dbUser) export moodledbpass=$(echo $json | jq -r .moodleProfile.dbPassword) export adminpass=$(echo $json | jq -r .moodleProfile.adminPassword) export dbadminlogin=$(echo $json | jq -r .dbServerProfile.adminLogin) export dbadminloginazure=$(echo $json | jq -r .dbServerProfile.adminLoginAzure) export dbadminpass=$(echo $json | jq -r .dbServerProfile.adminPassword) export storageAccountName=$(echo $json | jq -r .moodleProfile.storageAccountName) export storageAccountKey=$(echo $json | jq -r .moodleProfile.storageAccountKey) export azuremoodledbuser=$(echo $json | jq -r .moodleProfile.dbUserAzure) export redisDns=$(echo $json | jq -r .moodleProfile.redisDns) export redisAuth=$(echo $json | jq -r .moodleProfile.redisKey) export elasticVm1IP=$(echo $json | jq -r .moodleProfile.elasticVm1IP) export installO365pluginsSwitch=$(echo $json | jq -r .moodleProfile.installO365pluginsSwitch) export dbServerType=$(echo $json | jq -r .dbServerProfile.type) export fileServerType=$(echo $json | jq -r .fileServerProfile.type) export mssqlDbServiceObjectiveName=$(echo $json | jq -r .dbServerProfile.mssqlDbServiceObjectiveName) export mssqlDbEdition=$(echo $json | jq -r .dbServerProfile.mssqlDbEdition) export mssqlDbSize=$(echo $json | jq -r .dbServerProfile.mssqlDbSize) export installObjectFsSwitch=$(echo $json | jq -r .moodleProfile.installObjectFsSwitch) export installGdprPluginsSwitch=$(echo $json | jq -r .moodleProfile.installGdprPluginsSwitch) export thumbprintSslCert=$(echo $json | jq -r .siteProfile.thumbprintSslCert) export thumbprintCaCert=$(echo $json | jq -r .siteProfile.thumbprintCaCert) export searchType=$(echo $json | jq -r .moodleProfile.searchType) export azureSearchKey=$(echo $json | jq -r .moodleProfile.azureSearchKey) export azureSearchNameHost=$(echo $json | jq -r .moodleProfile.azureSearchNameHost) export tikaVmIP=$(echo $json | jq -r .moodleProfile.tikaVmIP) export syslogServer=$(echo $json | jq -r .moodleProfile.syslogServer) export webServerType=$(echo $json | jq -r .moodleProfile.webServerType) export htmlLocalCopySwitch=$(echo $json | jq -r .moodleProfile.htmlLocalCopySwitch) export nfsVmName=$(echo $json | jq -r .fileServerProfile.nfsVmName) export nfsHaLbIP=$(echo $json | jq -r .fileServerProfile.nfsHaLbIP) export nfsHaExportPath=$(echo $json | jq -r .fileServerProfile.nfsHaExportPath) export nfsByoIpExportPath=$(echo $json | jq -r .fileServerProfile.nfsByoIpExportPath) export storageAccountType=$(echo $json | jq -r .moodleProfile.storageAccountType) export fileServerDiskSize=$(echo $json | jq -r .fileServerProfile.fileServerDiskSize) export phpVersion=$(echo $json | jq -r .phpProfile.phpVersion) export isMigration=$(echo $json | jq -r .moodleProfile.isMigration) } function get_php_version { # Returns current PHP version, in the form of x.x, eg 7.0 or 7.2 if [ -z "$_PHPVER" ]; then _PHPVER=`/usr/bin/php -r "echo PHP_VERSION;" | /usr/bin/cut -c 1,2,3` fi echo $_PHPVER } function install_php_mssql_driver { # Download and build php/mssql driver export AZ_REPO=$(lsb_release -cs) export DEBIAN_FRONTEND='noninteractive' export NEEDRESTART_MODE='a' export ACCEPT_EULA='Y' mkdir -p /etc/apt/keyrings curl -sLS https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/keyrings/microsoft.gpg && chmod go+r /etc/apt/keyrings/microsoft.gpg echo "deb [arch=`dpkg --print-architecture` signed-by=/etc/apt/keyrings/microsoft.gpg] https://packages.microsoft.com/ubuntu/22.04/prod $AZ_REPO main" > /etc/apt/sources.list.d/mssql-release.list wait_for_process apt && \ apt_update_noninteractive && \ apt --yes --no-install-recommends -qq install msodbcsql18 mssql-tools18 unixodbc-dev >> /tmp/apt.log echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bash_profile echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bashrc source ~/.bashrc #Build mssql driver /usr/bin/pear config-set php_ini `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` system /usr/bin/pecl install sqlsrv /usr/bin/pecl install pdo_sqlsrv PHPVER=$(get_php_version) echo "extension=sqlsrv.so" >> /etc/php/$PHPVER/fpm/php.ini echo "extension=pdo_sqlsrv.so" >> /etc/php/$PHPVER/fpm/php.ini echo "extension=sqlsrv.so" >> /etc/php/$PHPVER/apache2/php.ini echo "extension=pdo_sqlsrv.so" >> /etc/php/$PHPVER/apache2/php.ini echo "extension=sqlsrv.so" >> /etc/php/$PHPVER/cli/php.ini echo "extension=pdo_sqlsrv.so" >> /etc/php/$PHPVER/cli/php.ini } function check_fileServerType_param { local fileServerType=$1 if [ "$fileServerType" != "gluster" -a "$fileServerType" != "azurefiles" -a "$fileServerType" != "nfs" -a "$fileServerType" != "nfs-ha" -a "$fileServerType" != "nfs-byo" ]; then echo "Invalid fileServerType ($fileServerType) given. Only 'gluster', 'azurefiles', 'nfs', 'nfs-ha' or 'nfs-byo' are allowed. Exiting" exit 1 fi } function create_azure_files_moodle_share { local storageAccountName=$1 local storageAccountKey=$2 local logFilePath=$3 local fileServerDiskSize=$4 az storage share create \ --name moodle \ --account-name $storageAccountName \ --account-key $storageAccountKey \ --fail-on-exist >>$logFilePath \ --quota $fileServerDiskSize } function replace_config_setting_value { local setting_name=$1 local setting_value=$2 local delemeter=$3 local file_name=$4 echo "Replacing $setting_name $delemeter $setting_value in $file_name" sed -i "s/^\($setting_name\s*$delemeter\s*\).*\$/\1$setting_value/" $file_name } # This function can replace only single line $CFG setting in moodle/config.php file. # Supported config setting format: # $CFG->setting = 'value'; # Usage: # replace_moodle_config_value "setting" "value" function replace_moodle_config_value { local formated_setting_name="\$CFG->$1" local formated_setting_value="'$2';" local delemeter="=" local moodle_config_file=/moodle/html/moodle/config.php replace_config_setting_value $formated_setting_name $formated_setting_value $delemeter $moodle_config_file } function check_azure_files_moodle_share_exists { local storageAccountName=$1 local storageAccountKey=$2 local azResponse=$(az storage share exists --name moodle --account-name $storageAccountName --account-key $storageAccountKey) if [ $? -ne 0 ];then echo "Could not check if moodle file share exists in the storage account ($storageAccountName)" exit 1 fi echo "az storage share exists command response:" echo $azResponse #Sample 'az storage share exists' command response # { "exists": true } local exists=$(echo $azResponse | jq -r .exists) if [ "$exists" != "true" ];then echo "File share 'moodle' does not exists in the storage account ($storageAccountName)" exit 1 fi } function setup_and_mount_gluster_moodle_share { local glusterNode=$1 local glusterVolume=$2 grep -q "/moodle.*glusterfs" /etc/fstab || echo -e $glusterNode':/'$glusterVolume' /moodle glusterfs defaults,_netdev,log-level=WARNING,log-file=/var/log/gluster.log 0 0' >> /etc/fstab mount /moodle } function setup_and_mount_azure_files_moodle_share { local storageAccountName=$1 local storageAccountKey=$2 cat < /etc/moodle_azure_files.credential username=$storageAccountName password=$storageAccountKey EOF chmod 600 /etc/moodle_azure_files.credential grep -q -s "^//$storageAccountName.file.core.windows.net/moodle\s\s*/moodle\s\s*cifs" /etc/fstab && _RET=$? || _RET=$? if [ $_RET != "0" ]; then echo -e "\n//$storageAccountName.file.core.windows.net/moodle /moodle cifs credentials=/etc/moodle_azure_files.credential,uid=www-data,gid=www-data,nofail,vers=3.0,dir_mode=0770,file_mode=0660,serverino,mfsymlinks" >> /etc/fstab fi mkdir -p /moodle mount /moodle } function setup_moodle_mount_dependency_for_systemd_service { local serviceName=$1 # E.g., nginx, apache2 if [ -z "$serviceName" ]; then return 1 fi local systemdSvcOverrideFileDir="/etc/systemd/system/${serviceName}.service.d" local systemdSvcOverrideFilePath="${systemdSvcOverrideFileDir}/moodle_on_azure_override.conf" grep -q -s "After=moodle.mount" $systemdSvcOverrideFilePath && _RET=$? || _RET=$? if [ $_RET != "0" ]; then mkdir -p $systemdSvcOverrideFileDir cat < $systemdSvcOverrideFilePath [Unit] After=moodle.mount [Service] LimitNOFILE=100000 EOF systemctl daemon-reload fi } # Functions for making NFS share available # TODO refactor these functions with the same ones in install_gluster.sh function scan_for_new_disks { local ALLOWLIST=${1} # E.g., /dev/sda|/dev/sdb declare -a RET local DEVS=$(ls -1 /dev/disk/azure/scsi1/lun*|egrep "${ALLOWLIST}"|egrep "[0-9]$") for DEV in ${DEVS}; do # Check each device if there is a "1" partition. If not, # "assume" it is not partitioned. if [ ! -b ${DEV}1 ]; then RET+="${DEV} " fi done echo "${RET}" } function create_raid0_ubuntu { local RAIDDISK=${1} # E.g., /dev/md1 local RAIDCHUNKSIZE=${2} # E.g., 128 local DISKCOUNT=${3} # E.g., 4 shift shift shift local DISKS="$@" dpkg -s mdadm && _RET=$? || _RET=$? if [ $_RET -eq 1 ]; then echo "installing mdadm" apt_install_noninteractive mdadm fi echo "Creating raid0" udevadm control --stop-exec-queue echo "yes" | mdadm --create $RAIDDISK --name=data --level=0 --chunk=$RAIDCHUNKSIZE --raid-devices=$DISKCOUNT $DISKS udevadm control --start-exec-queue mdadm --detail --verbose --scan > /etc/mdadm/mdadm.conf } function do_partition { # This function creates one (1) primary partition on the # disk device, using all available space local DISK=${1} # E.g., /dev/sdc echo "Partitioning disk $DISK" echo -ne "n\np\n1\n\n\nw\n" | fdisk "${DISK}" #> /dev/null 2>&1 # # Use the bash-specific $PIPESTATUS to ensure we get the correct exit code # from fdisk and not from echo if [ ${PIPESTATUS[1]} -ne 0 ]; then echo "An error occurred partitioning ${DISK}" >&2 echo "I cannot continue" >&2 exit 2 fi } function add_local_filesystem_to_fstab { local UUID=${1} local MOUNTPOINT=${2} # E.g., /moodle grep -q -s "${UUID}" /etc/fstab && _RET=$? || _RET=$? if [ $_RET -eq 0 ]; then echo "Not adding ${UUID} to fstab again (it's already there!)" else LINE="\nUUID=${UUID} ${MOUNTPOINT} ext4 defaults,noatime 0 0" echo -e "${LINE}" >> /etc/fstab fi } function setup_raid_disk_and_filesystem { local MOUNTPOINT=${1} # E.g., /moodle local RAIDDISK=${2} # E.g., /dev/md1 local RAIDPARTITION=${3} # E.g., /dev/md1p1 local CREATE_FILESYSTEM=${4} # E.g., "" (true) or any non-empty string (false) local DISKS=$(scan_for_new_disks "/dev/disk/azure/scsi1/lun0|/dev/disk/azure/scsi1/lun1") echo "Disks are ${DISKS}" declare -i DISKCOUNT local DISKCOUNT=$(echo "$DISKS" | wc -w) echo "Disk count is $DISKCOUNT" if [ $DISKCOUNT = "0" ]; then echo "No new (unpartitioned) disks available... Returning non-zero..." return 1 fi if [ $DISKCOUNT -gt 1 ]; then create_raid0_ubuntu ${RAIDDISK} 128 $DISKCOUNT $DISKS AZMDL_DISK=$RAIDDISK if [ -z "$CREATE_FILESYSTEM" ]; then do_partition ${RAIDDISK} local PARTITION="${RAIDPARTITION}" fi else # Just one unpartitioned disk AZMDL_DISK=$DISKS if [ -z "$CREATE_FILESYSTEM" ]; then do_partition ${DISKS} local PARTITION=$(fdisk -l ${DISKS}|grep -A 1 Device|tail -n 1|awk '{print $1}') fi fi echo "Disk (RAID if multiple unpartitioned disks, or as is if only one unpartitioned disk) is set up, and env var AZMDL_DISK is set to '$AZMDL_DISK' for later reference" if [ -z "$CREATE_FILESYSTEM" ]; then echo "Creating filesystem on ${PARTITION}." mkfs -t ext4 ${PARTITION} mkdir -p "${MOUNTPOINT}" local UUID=$(blkid -u filesystem ${PARTITION}|awk -F "[= ]" '{print $3}'|tr -d "\"") add_local_filesystem_to_fstab "${UUID}" "${MOUNTPOINT}" echo "Mounting disk ${PARTITION} on ${MOUNTPOINT}" mount "${MOUNTPOINT}" fi } function configure_nfs_server_and_export { local MOUNTPOINT=${1} # E.g., /moodle echo "Installing nfs server..." apt_install_noninteractive nfs-kernel-server echo "Exporting ${MOUNTPOINT}..." grep -q -s "^${MOUNTPOINT}" /etc/exports && _RET=$? || _RET=$? if [ $_RET = "0" ]; then echo "${MOUNTPOINT} is already exported. Returning..." else echo -e "\n${MOUNTPOINT} *(rw,sync,no_root_squash)" >> /etc/exports systemctl restart nfs-kernel-server.service fi } function configure_nfs_client_and_mount0 { local NFS_HOST_EXPORT_PATH=${1} # E.g., controller-vm-ab12cd:/moodle or 172.16.3.100:/drbd/data local MOUNTPOINT=${2} # E.g., /moodle apt install -y nfs-common mkdir -p ${MOUNTPOINT} grep -q -s "^${NFS_HOST_EXPORT_PATH}" /etc/fstab && _RET=$? || _RET=$? if [ $_RET = "0" ]; then echo "${NFS_HOST_EXPORT_PATH} already in /etc/fstab... skipping to add" else echo -e "\n${NFS_HOST_EXPORT_PATH} ${MOUNTPOINT} nfs auto 0 0" >> /etc/fstab fi mount ${MOUNTPOINT} } function configure_nfs_client_and_mount { local NFS_SERVER=${1} # E.g., controller-vm-ab12cd or IP (NFS-HA LB) local NFS_DIR=${2} # E.g., /moodle or /drbd/data local MOUNTPOINT=${3} # E.g., /moodle configure_nfs_client_and_mount0 "${NFS_SERVER}:${NFS_DIR}" ${MOUNTPOINT} } SERVER_TIMESTAMP_FULLPATH="/moodle/html/moodle/.last_modified_time.moodle_on_azure" LOCAL_TIMESTAMP_FULLPATH="/var/www/html/moodle/.last_modified_time.moodle_on_azure" # Create a script to sync /moodle/html/moodle (gluster/NFS) and /var/www/html/moodle (local) and set up a minutely cron job # Should be called by root and only on a VMSS web frontend VM function setup_html_local_copy_cron_job { if [ "$(whoami)" != "root" ]; then echo "${0}: Must be run as root!" return 1 fi local SYNC_SCRIPT_FULLPATH="/usr/local/bin/sync_moodle_html_local_copy_if_modified.sh" mkdir -p $(dirname ${SYNC_SCRIPT_FULLPATH}) local SYNC_LOG_FULLPATH="/var/log/moodle-html-sync.log" cat < ${SYNC_SCRIPT_FULLPATH} #!/bin/bash sleep \$((\$RANDOM%30)) if [ -f "$SERVER_TIMESTAMP_FULLPATH" ]; then SERVER_TIMESTAMP=\$(cat $SERVER_TIMESTAMP_FULLPATH) if [ -f "$LOCAL_TIMESTAMP_FULLPATH" ]; then LOCAL_TIMESTAMP=\$(cat $LOCAL_TIMESTAMP_FULLPATH) else logger -p local2.notice -t moodle "Local timestamp file ($LOCAL_TIMESTAMP_FULLPATH) does not exist. Probably first time syncing? Continuing to sync." mkdir -p /var/www/html fi if [ "\$SERVER_TIMESTAMP" != "\$LOCAL_TIMESTAMP" ]; then logger -p local2.notice -t moodle "Server time stamp (\$SERVER_TIMESTAMP) is different from local time stamp (\$LOCAL_TIMESTAMP). Start syncing..." if [[ \$(find $SYNC_LOG_FULLPATH -type f -size +20M 2> /dev/null) ]]; then truncate -s 0 $SYNC_LOG_FULLPATH fi echo \$(date +%Y%m%d%H%M%S) >> $SYNC_LOG_FULLPATH rsync -av --delete /moodle/html/moodle /var/www/html >> $SYNC_LOG_FULLPATH fi else logger -p local2.notice -t moodle "Remote timestamp file ($SERVER_TIMESTAMP_FULLPATH) does not exist. Is /moodle mounted? Exiting with error." exit 1 fi EOF chmod 500 ${SYNC_SCRIPT_FULLPATH} local CRON_DESC_FULLPATH="/etc/cron.d/sync-moodle-html-local-copy" cat < ${CRON_DESC_FULLPATH} * * * * * root ${SYNC_SCRIPT_FULLPATH} EOF chmod 644 ${CRON_DESC_FULLPATH} # Addition of a hook for custom script run on VMSS from shared mount to allow customised configuration of the VMSS as required local CRON_DESC_FULLPATH2="/etc/cron.d/update-vmss-config" cat < ${CRON_DESC_FULLPATH2} * * * * * root [ -f /moodle/bin/update-vmss-config ] && /bin/bash /moodle/bin/update-vmss-config EOF chmod 644 ${CRON_DESC_FULLPATH2} } LAST_MODIFIED_TIME_UPDATE_SCRIPT_FULLPATH="/usr/local/bin/update_last_modified_time.moodle_on_azure.sh" # Create a script to modify the last modified timestamp file (/moodle/html/moodle/last_modified_time.moodle_on_azure) # Should be called by root and only on the controller VM. # The moodle admin should run the generated script everytime the /moodle/html/moodle directory content is updated (e.g., moodle upgrade, config change or plugin install/upgrade) function create_last_modified_time_update_script { if [ "$(whoami)" != "root" ]; then echo "${0}: Must be run as root!" return 1 fi mkdir -p $(dirname $LAST_MODIFIED_TIME_UPDATE_SCRIPT_FULLPATH) cat < $LAST_MODIFIED_TIME_UPDATE_SCRIPT_FULLPATH #!/bin/bash echo \$(date +%Y%m%d%H%M%S) > $SERVER_TIMESTAMP_FULLPATH EOF chmod +x $LAST_MODIFIED_TIME_UPDATE_SCRIPT_FULLPATH } function run_once_last_modified_time_update_script { $LAST_MODIFIED_TIME_UPDATE_SCRIPT_FULLPATH } # O365 plugins are released only for 'MOODLE_xy_STABLE', # whereas we want to support the Moodle tagged versions (e.g., 'v3.4.2'). # This function helps getting the stable version # (for O365 plugin ver.) # from a Moodle version tag. This utility function recognizes tag names # of the form 'vx.y.z' only. function get_o365plugin_version_from_moodle_version { local moodleVersion=${1} if [[ "$moodleVersion" =~ v([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then echo "MOODLE_${BASH_REMATCH[1]}${BASH_REMATCH[2]}_STABLE" else echo $moodleVersion fi } # For Moodle tags (e.g., "v3.4.2"), the unzipped Moodle dir is no longer # "moodle-$moodleVersion", because for tags, it's without "v". That is, # it's "moodle-3.4.2". Therefore, we need a separate helper function for that... function get_moodle_unzip_dir_from_moodle_version { local moodleVersion=${1} if [[ "$moodleVersion" =~ v([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then echo "moodle-${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" else echo "moodle-$moodleVersion" fi } # Long Redis cache Moodle config file generation code moved here function create_redis_configuration_in_moodledata_muc_config_php { # create redis configuration in /moodle/moodledata/muc/config.php cat < /moodle/moodledata/muc/config.php '7a142be09ea65699e4a6f6ef91c0773c', 'stores' => array ( 'default_application' => array ( 'name' => 'default_application', 'plugin' => 'file', 'configuration' => array ( ), 'features' => 30, 'modes' => 3, 'default' => true, 'class' => 'cachestore_file', 'lock' => 'cachelock_file_default', ), 'default_session' => array ( 'name' => 'default_session', 'plugin' => 'session', 'configuration' => array ( ), 'features' => 14, 'modes' => 2, 'default' => true, 'class' => 'cachestore_session', 'lock' => 'cachelock_file_default', ), 'default_request' => array ( 'name' => 'default_request', 'plugin' => 'static', 'configuration' => array ( ), 'features' => 31, 'modes' => 4, 'default' => true, 'class' => 'cachestore_static', 'lock' => 'cachelock_file_default', ), 'redis' => array ( 'name' => 'redis', 'plugin' => 'redis', 'configuration' => array ( 'server' => '$redisDns', 'prefix' => 'moodle_prod', 'password' => '$redisAuth', 'serializer' => '1', ), 'features' => 26, 'modes' => 3, 'mappingsonly' => false, 'class' => 'cachestore_redis', 'default' => false, 'lock' => 'cachelock_file_default', ), 'local_file' => array ( 'name' => 'local_file', 'plugin' => 'file', 'configuration' => array ( 'path' => '/tmp/muc/moodle_prod', 'autocreate' => 1, ), 'features' => 30, 'modes' => 3, 'mappingsonly' => false, 'class' => 'cachestore_file', 'default' => false, 'lock' => 'cachelock_file_default', ), ), 'modemappings' => array ( 0 => array ( 'store' => 'redis', 'mode' => 1, 'sort' => 0, ), 1 => array ( 'store' => 'default_session', 'mode' => 2, 'sort' => 0, ), 2 => array ( 'store' => 'default_request', 'mode' => 4, 'sort' => 0, ), ), 'definitions' => array ( 'core/string' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 30, 'canuselocalstore' => true, 'component' => 'core', 'area' => 'string', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/langmenu' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'canuselocalstore' => true, 'component' => 'core', 'area' => 'langmenu', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/databasemeta' => array ( 'mode' => 1, 'requireidentifiers' => array ( 0 => 'dbfamily', ), 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 15, 'component' => 'core', 'area' => 'databasemeta', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/eventinvalidation' => array ( 'mode' => 1, 'staticacceleration' => true, 'requiredataguarantee' => true, 'simpledata' => true, 'component' => 'core', 'area' => 'eventinvalidation', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/questiondata' => array ( 'mode' => 1, 'simplekeys' => true, 'requiredataguarantee' => false, 'datasource' => 'question_finder', 'datasourcefile' => 'question/engine/bank.php', 'component' => 'core', 'area' => 'questiondata', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/htmlpurifier' => array ( 'mode' => 1, 'canuselocalstore' => true, 'component' => 'core', 'area' => 'htmlpurifier', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/config' => array ( 'mode' => 1, 'staticacceleration' => true, 'simpledata' => true, 'component' => 'core', 'area' => 'config', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/groupdata' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 2, 'component' => 'core', 'area' => 'groupdata', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/calendar_subscriptions' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'component' => 'core', 'area' => 'calendar_subscriptions', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/capabilities' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 1, 'ttl' => 3600, 'component' => 'core', 'area' => 'capabilities', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/yuimodules' => array ( 'mode' => 1, 'component' => 'core', 'area' => 'yuimodules', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/observers' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 2, 'component' => 'core', 'area' => 'observers', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/plugin_manager' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'component' => 'core', 'area' => 'plugin_manager', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/coursecattree' => array ( 'mode' => 1, 'staticacceleration' => true, 'invalidationevents' => array ( 0 => 'changesincoursecat', ), 'component' => 'core', 'area' => 'coursecattree', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/coursecat' => array ( 'mode' => 2, 'invalidationevents' => array ( 0 => 'changesincoursecat', 1 => 'changesincourse', ), 'ttl' => 600, 'component' => 'core', 'area' => 'coursecat', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/coursecatrecords' => array ( 'mode' => 4, 'simplekeys' => true, 'invalidationevents' => array ( 0 => 'changesincoursecat', ), 'component' => 'core', 'area' => 'coursecatrecords', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/coursecontacts' => array ( 'mode' => 1, 'staticacceleration' => true, 'simplekeys' => true, 'ttl' => 3600, 'component' => 'core', 'area' => 'coursecontacts', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/repositories' => array ( 'mode' => 4, 'component' => 'core', 'area' => 'repositories', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/externalbadges' => array ( 'mode' => 1, 'simplekeys' => true, 'ttl' => 3600, 'component' => 'core', 'area' => 'externalbadges', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/coursemodinfo' => array ( 'mode' => 1, 'simplekeys' => true, 'canuselocalstore' => true, 'component' => 'core', 'area' => 'coursemodinfo', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/userselections' => array ( 'mode' => 2, 'simplekeys' => true, 'simpledata' => true, 'component' => 'core', 'area' => 'userselections', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/completion' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'ttl' => 3600, 'staticacceleration' => true, 'staticaccelerationsize' => 2, 'component' => 'core', 'area' => 'completion', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/coursecompletion' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'ttl' => 3600, 'staticacceleration' => true, 'staticaccelerationsize' => 30, 'component' => 'core', 'area' => 'coursecompletion', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/navigation_expandcourse' => array ( 'mode' => 2, 'simplekeys' => true, 'simpledata' => true, 'component' => 'core', 'area' => 'navigation_expandcourse', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/suspended_userids' => array ( 'mode' => 4, 'simplekeys' => true, 'simpledata' => true, 'component' => 'core', 'area' => 'suspended_userids', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/roledefs' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 30, 'component' => 'core', 'area' => 'roledefs', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/plugin_functions' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 5, 'component' => 'core', 'area' => 'plugin_functions', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/tags' => array ( 'mode' => 4, 'simplekeys' => true, 'staticacceleration' => true, 'component' => 'core', 'area' => 'tags', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/grade_categories' => array ( 'mode' => 2, 'simplekeys' => true, 'invalidationevents' => array ( 0 => 'changesingradecategories', ), 'component' => 'core', 'area' => 'grade_categories', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/temp_tables' => array ( 'mode' => 4, 'simplekeys' => true, 'simpledata' => true, 'component' => 'core', 'area' => 'temp_tables', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/tagindexbuilder' => array ( 'mode' => 2, 'simplekeys' => true, 'simplevalues' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 10, 'ttl' => 900, 'invalidationevents' => array ( 0 => 'resettagindexbuilder', ), 'component' => 'core', 'area' => 'tagindexbuilder', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'core/contextwithinsights' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 1, 'component' => 'core', 'area' => 'contextwithinsights', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/message_processors_enabled' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 3, 'component' => 'core', 'area' => 'message_processors_enabled', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/message_time_last_message_between_users' => array ( 'mode' => 1, 'simplekeys' => true, 'simplevalues' => true, 'datasource' => '\\core_message\\time_last_message_between_users', 'component' => 'core', 'area' => 'message_time_last_message_between_users', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/fontawesomeiconmapping' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 1, 'component' => 'core', 'area' => 'fontawesomeiconmapping', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/postprocessedcss' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => false, 'component' => 'core', 'area' => 'postprocessedcss', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'core/user_group_groupings' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'component' => 'core', 'area' => 'user_group_groupings', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'availability_grade/scores' => array ( 'mode' => 1, 'staticacceleration' => true, 'staticaccelerationsize' => 2, 'ttl' => 3600, 'component' => 'availability_grade', 'area' => 'scores', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'availability_grade/items' => array ( 'mode' => 1, 'staticacceleration' => true, 'staticaccelerationsize' => 2, 'ttl' => 3600, 'component' => 'availability_grade', 'area' => 'items', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'mod_glossary/concepts' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => false, 'staticacceleration' => true, 'staticaccelerationsize' => 30, 'component' => 'mod_glossary', 'area' => 'concepts', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'repository_googledocs/folder' => array ( 'mode' => 1, 'simplekeys' => false, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 10, 'canuselocalstore' => true, 'component' => 'repository_googledocs', 'area' => 'folder', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'repository_onedrive/folder' => array ( 'mode' => 1, 'simplekeys' => false, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 10, 'canuselocalstore' => true, 'component' => 'repository_onedrive', 'area' => 'folder', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'repository_skydrive/foldername' => array ( 'mode' => 2, 'component' => 'repository_skydrive', 'area' => 'foldername', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'tool_mobile/plugininfo' => array ( 'mode' => 1, 'simplekeys' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 1, 'component' => 'tool_mobile', 'area' => 'plugininfo', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'tool_monitor/eventsubscriptions' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 10, 'component' => 'tool_monitor', 'area' => 'eventsubscriptions', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'tool_uploadcourse/helper' => array ( 'mode' => 4, 'component' => 'tool_uploadcourse', 'area' => 'helper', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 2, ), 'tool_usertours/tourdata' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 1, 'component' => 'tool_usertours', 'area' => 'tourdata', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), 'tool_usertours/stepdata' => array ( 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 1, 'component' => 'tool_usertours', 'area' => 'stepdata', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ), ), 'definitionmappings' => array ( 0 => array ( 'store' => 'local_file', 'definition' => 'core/coursemodinfo', 'sort' => 1, ), 1 => array ( 'store' => 'redis', 'definition' => 'core/groupdata', 'sort' => 1, ), 2 => array ( 'store' => 'redis', 'definition' => 'core/roledefs', 'sort' => 1, ), 3 => array ( 'store' => 'redis', 'definition' => 'tool_usertours/tourdata', 'sort' => 1, ), 4 => array ( 'store' => 'redis', 'definition' => 'repository_onedrive/folder', 'sort' => 1, ), 5 => array ( 'store' => 'redis', 'definition' => 'core/message_processors_enabled', 'sort' => 1, ), 6 => array ( 'store' => 'redis', 'definition' => 'core/coursecontacts', 'sort' => 1, ), 7 => array ( 'store' => 'redis', 'definition' => 'repository_googledocs/folder', 'sort' => 1, ), 8 => array ( 'store' => 'redis', 'definition' => 'core/questiondata', 'sort' => 1, ), 9 => array ( 'store' => 'redis', 'definition' => 'core/coursecat', 'sort' => 1, ), 10 => array ( 'store' => 'redis', 'definition' => 'core/databasemeta', 'sort' => 1, ), 11 => array ( 'store' => 'redis', 'definition' => 'core/eventinvalidation', 'sort' => 1, ), 12 => array ( 'store' => 'redis', 'definition' => 'core/coursecattree', 'sort' => 1, ), 13 => array ( 'store' => 'redis', 'definition' => 'core/coursecompletion', 'sort' => 1, ), 14 => array ( 'store' => 'redis', 'definition' => 'core/user_group_groupings', 'sort' => 1, ), 15 => array ( 'store' => 'redis', 'definition' => 'core/capabilities', 'sort' => 1, ), 16 => array ( 'store' => 'redis', 'definition' => 'core/yuimodules', 'sort' => 1, ), 17 => array ( 'store' => 'redis', 'definition' => 'core/observers', 'sort' => 1, ), 18 => array ( 'store' => 'redis', 'definition' => 'mod_glossary/concepts', 'sort' => 1, ), 19 => array ( 'store' => 'redis', 'definition' => 'core/fontawesomeiconmapping', 'sort' => 1, ), 20 => array ( 'store' => 'redis', 'definition' => 'core/config', 'sort' => 1, ), 21 => array ( 'store' => 'redis', 'definition' => 'tool_mobile/plugininfo', 'sort' => 1, ), 22 => array ( 'store' => 'redis', 'definition' => 'core/plugin_functions', 'sort' => 1, ), 23 => array ( 'store' => 'redis', 'definition' => 'core/postprocessedcss', 'sort' => 1, ), 24 => array ( 'store' => 'redis', 'definition' => 'core/plugin_manager', 'sort' => 1, ), 25 => array ( 'store' => 'redis', 'definition' => 'tool_usertours/stepdata', 'sort' => 1, ), 26 => array ( 'store' => 'redis', 'definition' => 'availability_grade/items', 'sort' => 1, ), 27 => array ( 'store' => 'local_file', 'definition' => 'core/string', 'sort' => 1, ), 28 => array ( 'store' => 'redis', 'definition' => 'core/externalbadges', 'sort' => 1, ), 29 => array ( 'store' => 'local_file', 'definition' => 'core/langmenu', 'sort' => 1, ), 30 => array ( 'store' => 'local_file', 'definition' => 'core/htmlpurifier', 'sort' => 1, ), 31 => array ( 'store' => 'redis', 'definition' => 'core/completion', 'sort' => 1, ), 32 => array ( 'store' => 'redis', 'definition' => 'core/calendar_subscriptions', 'sort' => 1, ), 33 => array ( 'store' => 'redis', 'definition' => 'core/contextwithinsights', 'sort' => 1, ), 34 => array ( 'store' => 'redis', 'definition' => 'tool_monitor/eventsubscriptions', 'sort' => 1, ), 35 => array ( 'store' => 'redis', 'definition' => 'core/message_time_last_message_between_users', 'sort' => 1, ), 36 => array ( 'store' => 'redis', 'definition' => 'availability_grade/scores', 'sort' => 1, ), ), 'locks' => array ( 'cachelock_file_default' => array ( 'name' => 'cachelock_file_default', 'type' => 'cachelock_file', 'dir' => 'filelocks', 'default' => true, ), ), ); EOF } # Long fail2ban config command moved here function config_fail2ban { cat < /etc/fail2ban/jail.conf # Fail2Ban configuration file. # # This file was composed for Debian systems from the original one # provided now under /usr/share/doc/fail2ban/examples/jail.conf # for additional examples. # # Comments: use '#' for comment lines and ';' for inline comments # # To avoid merges during upgrades DO NOT MODIFY THIS FILE # and rather provide your changes in /etc/fail2ban/jail.local # # The DEFAULT allows a global definition of the options. They can be overridden # in each jail afterwards. [DEFAULT] # "ignoreip" can be an IP address, a CIDR mask or a DNS host. Fail2ban will not # ban a host which matches an address in this list. Several addresses can be # defined using space separator. ignoreip = 127.0.0.1/8 # "bantime" is the number of seconds that a host is banned. bantime = 600 # A host is banned if it has generated "maxretry" during the last "findtime" # seconds. findtime = 600 maxretry = 3 # "backend" specifies the backend used to get files modification. # Available options are "pyinotify", "gamin", "polling" and "auto". # This option can be overridden in each jail as well. # # pyinotify: requires pyinotify (a file alteration monitor) to be installed. # If pyinotify is not installed, Fail2ban will use auto. # gamin: requires Gamin (a file alteration monitor) to be installed. # If Gamin is not installed, Fail2ban will use auto. # polling: uses a polling algorithm which does not require external libraries. # auto: will try to use the following backends, in order: # pyinotify, gamin, polling. backend = auto # "usedns" specifies if jails should trust hostnames in logs, # warn when reverse DNS lookups are performed, or ignore all hostnames in logs # # yes: if a hostname is encountered, a reverse DNS lookup will be performed. # warn: if a hostname is encountered, a reverse DNS lookup will be performed, # but it will be logged as a warning. # no: if a hostname is encountered, will not be used for banning, # but it will be logged as info. usedns = warn # # Destination email address used solely for the interpolations in # jail.{conf,local} configuration files. destemail = root@localhost # # Name of the sender for mta actions sendername = Fail2Ban # # ACTIONS # # Default banning action (e.g. iptables, iptables-new, # iptables-multiport, shorewall, etc) It is used to define # action_* variables. Can be overridden globally or per # section within jail.local file banaction = iptables-multiport # email action. Since 0.8.1 upstream fail2ban uses sendmail # MTA for the mailing. Change mta configuration parameter to mail # if you want to revert to conventional 'mail'. mta = sendmail # Default protocol protocol = tcp # Specify chain where jumps would need to be added in iptables-* actions chain = INPUT # # Action shortcuts. To be used to define action parameter # The simplest action to take: ban only action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report to the destemail. action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s", sendername="%(sendername)s"] # ban & send an e-mail with whois report and relevant log lines # to the destemail. action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] %(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s", sendername="%(sendername)s"] # Choose default action. To change, just override value of 'action' with the # interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local # globally (section [DEFAULT]) or per specific section action = %(action_)s # # JAILS # # Next jails corresponds to the standard configuration in Fail2ban 0.6 which # was shipped in Debian. Enable any defined here jail by including # # [SECTION_NAME] # enabled = true # # in /etc/fail2ban/jail.local. # # Optionally you may override any other parameter (e.g. banaction, # action, port, logpath, etc) in that section within jail.local [ssh] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 6 [dropbear] enabled = false port = ssh filter = dropbear logpath = /var/log/auth.log maxretry = 6 # Generic filter for pam. Has to be used with action which bans all ports # such as iptables-allports, shorewall [pam-generic] enabled = false # pam-generic filter can be customized to monitor specific subset of 'tty's filter = pam-generic # port actually must be irrelevant but lets leave it all for some possible uses port = all banaction = iptables-allports port = anyport logpath = /var/log/auth.log maxretry = 6 [xinetd-fail] enabled = false filter = xinetd-fail port = all banaction = iptables-multiport-log logpath = /var/log/daemon.log maxretry = 2 [ssh-ddos] enabled = false port = ssh filter = sshd-ddos logpath = /var/log/auth.log maxretry = 6 # Here we use blackhole routes for not requiring any additional kernel support # to store large volumes of banned IPs [ssh-route] enabled = false filter = sshd action = route logpath = /var/log/sshd.log maxretry = 6 # Here we use a combination of Netfilter/Iptables and IPsets # for storing large volumes of banned IPs # # IPset comes in two versions. See ipset -V for which one to use # requires the ipset package and kernel support. [ssh-iptables-ipset4] enabled = false port = ssh filter = sshd banaction = iptables-ipset-proto4 logpath = /var/log/sshd.log maxretry = 6 [ssh-iptables-ipset6] enabled = false port = ssh filter = sshd banaction = iptables-ipset-proto6 logpath = /var/log/sshd.log maxretry = 6 # # HTTP servers # [apache] enabled = false port = http,https filter = apache-auth logpath = /var/log/apache*/*error.log maxretry = 6 # default action is now multiport, so apache-multiport jail was left # for compatibility with previous (<0.7.6-2) releases [apache-multiport] enabled = false port = http,https filter = apache-auth logpath = /var/log/apache*/*error.log maxretry = 6 [apache-noscript] enabled = false port = http,https filter = apache-noscript logpath = /var/log/apache*/*error.log maxretry = 6 [apache-overflows] enabled = false port = http,https filter = apache-overflows logpath = /var/log/apache*/*error.log maxretry = 2 # Ban attackers that try to use PHP's URL-fopen() functionality # through GET/POST variables. - Experimental, with more than a year # of usage in production environments. [php-url-fopen] enabled = false port = http,https filter = php-url-fopen logpath = /var/www/*/logs/access_log # A simple PHP-fastcgi jail which works with lighttpd. # If you run a lighttpd server, then you probably will # find these kinds of messages in your error_log: # ALERT – tried to register forbidden variable ‘GLOBALS’ # through GET variables (attacker '1.2.3.4', file '/var/www/default/htdocs/index.php') [lighttpd-fastcgi] enabled = false port = http,https filter = lighttpd-fastcgi logpath = /var/log/lighttpd/error.log # Same as above for mod_auth # It catches wrong authentifications [lighttpd-auth] enabled = false port = http,https filter = suhosin logpath = /var/log/lighttpd/error.log [nginx-http-auth] enabled = false filter = nginx-http-auth port = http,https logpath = /var/log/nginx/error.log # Monitor roundcube server [roundcube-auth] enabled = false filter = roundcube-auth port = http,https logpath = /var/log/roundcube/userlogins [sogo-auth] enabled = false filter = sogo-auth port = http, https # without proxy this would be: # port = 20000 logpath = /var/log/sogo/sogo.log # # FTP servers # [vsftpd] enabled = false port = ftp,ftp-data,ftps,ftps-data filter = vsftpd logpath = /var/log/vsftpd.log # or overwrite it in jails.local to be # logpath = /var/log/auth.log # if you want to rely on PAM failed login attempts # vsftpd's failregex should match both of those formats maxretry = 6 [proftpd] enabled = false port = ftp,ftp-data,ftps,ftps-data filter = proftpd logpath = /var/log/proftpd/proftpd.log maxretry = 6 [pure-ftpd] enabled = false port = ftp,ftp-data,ftps,ftps-data filter = pure-ftpd logpath = /var/log/syslog maxretry = 6 [wuftpd] enabled = false port = ftp,ftp-data,ftps,ftps-data filter = wuftpd logpath = /var/log/syslog maxretry = 6 # # Mail servers # [postfix] enabled = false port = smtp,ssmtp,submission filter = postfix logpath = /var/log/mail.log [couriersmtp] enabled = false port = smtp,ssmtp,submission filter = couriersmtp logpath = /var/log/mail.log # # Mail servers authenticators: might be used for smtp,ftp,imap servers, so # all relevant ports get banned # [courierauth] enabled = false port = smtp,ssmtp,submission,imap2,imap3,imaps,pop3,pop3s filter = courierlogin logpath = /var/log/mail.log [sasl] enabled = false port = smtp,ssmtp,submission,imap2,imap3,imaps,pop3,pop3s filter = postfix-sasl # You might consider monitoring /var/log/mail.warn instead if you are # running postfix since it would provide the same log lines at the # "warn" level but overall at the smaller filesize. logpath = /var/log/mail.log [dovecot] enabled = false port = smtp,ssmtp,submission,imap2,imap3,imaps,pop3,pop3s filter = dovecot logpath = /var/log/mail.log # To log wrong MySQL access attempts add to /etc/my.cnf: # log-error=/var/log/mysqld.log # log-warning = 2 [mysqld-auth] enabled = false filter = mysqld-auth port = 3306 logpath = /var/log/mysqld.log # DNS Servers # These jails block attacks against named (bind9). By default, logging is off # with bind9 installation. You will need something like this: # # logging { # channel security_file { # file "/var/log/named/security.log" versions 3 size 30m; # severity dynamic; # print-time yes; # }; # category security { # security_file; # }; # }; # # in your named.conf to provide proper logging # !!! WARNING !!! # Since UDP is connection-less protocol, spoofing of IP and imitation # of illegal actions is way too simple. Thus enabling of this filter # might provide an easy way for implementing a DoS against a chosen # victim. See # http://nion.modprobe.de/blog/archives/690-fail2ban-+-dns-fail.html # Please DO NOT USE this jail unless you know what you are doing. #[named-refused-udp] # #enabled = false #port = domain,953 #protocol = udp #filter = named-refused #logpath = /var/log/named/security.log [named-refused-tcp] enabled = false port = domain,953 protocol = tcp filter = named-refused logpath = /var/log/named/security.log # Multiple jails, 1 per protocol, are necessary ATM: # see https://github.com/fail2ban/fail2ban/issues/37 [asterisk-tcp] enabled = false filter = asterisk port = 5060,5061 protocol = tcp logpath = /var/log/asterisk/messages [asterisk-udp] enabled = false filter = asterisk port = 5060,5061 protocol = udp logpath = /var/log/asterisk/messages # Jail for more extended banning of persistent abusers # !!! WARNING !!! # Make sure that your loglevel specified in fail2ban.conf/.local # is not at DEBUG level -- which might then cause fail2ban to fall into # an infinite loop constantly feeding itself with non-informative lines [recidive] enabled = false filter = recidive logpath = /var/log/fail2ban.log action = iptables-allports[name=recidive] sendmail-whois-lines[name=recidive, logpath=/var/log/fail2ban.log] bantime = 604800 ; 1 week findtime = 86400 ; 1 day maxretry = 5 EOF } ================================================ FILE: scripts/install_elastic.sh ================================================ #!/bin/bash # Custom Script for Linux # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. esClusterName=$1 elasticvm1ip=$2 elasticvm2ip=$3 elasticvm3ip=$4 echo $esClusterName >> /tmp/vars.txt echo $elasticvm1ip >> /tmp/vars.txt echo $elasticvm2ip >> /tmp/vars.txt echo $elasticvm3ip >> /tmp/vars.txt function wait_for_process { until [ -z $(/usr/bin/pgrep ${1}) ]; do printf '.' sleep 0.5 done } function apt_update_noninteractive { export DEBIAN_FRONTEND='noninteractive' # waiting for apt to finish before running any other commands wait_for_process apt; apt --yes -qq -o=Dpkg::Use-Pty=0 update } function apt_install_noninteractive { export DEBIAN_FRONTEND='noninteractive' export NEEDRESTART_MODE='a' export ACCEPT_EULA='Y' # waiting for apt to finish before running any other commands wait_for_process apt; apt --yes --no-install-recommends -qq -o=Dpkg::Use-Pty=0 -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install "${@}" } { # make sure the system does automatic update apt_update_noninteractive apt_install_noninteractive unattended-upgrades apt-transport-https # configure elastic search repository & install elastic search wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/5.x/apt stable main" | tee /etc/apt/sources.list.d/elastic-5.x.list apt_update_noninteractive apt_install_noninteractive elasticsearch=5.6.16 # install the required packages apt_install_noninteractive openjdk-8-jre openjdk-8-jdk default-jre default-jdk # Configure elasticsearch cat < /etc/elasticsearch/elasticsearch.yml # ======================== Elasticsearch Configuration ========================= # # NOTE: Elasticsearch comes with reasonable defaults for most settings. # Before you set out to tweak and tune the configuration, make sure you # understand what are you trying to accomplish and the consequences. # # The primary way of configuring a node is via this file. This template lists # the most important settings you may want to configure for a production cluster. # # Please consult the documentation for further information on configuration options: # https://www.elastic.co/guide/en/elasticsearch/reference/index.html # # ---------------------------------- Cluster ----------------------------------- # # Use a descriptive name for your cluster: # cluster.name: ${esClusterName} # # ------------------------------------ Node ------------------------------------ # # Use a descriptive name for the node: # node.name: ${HOSTNAME} # # Add custom attributes to the node: # #node.attr.rack: r1 # # ----------------------------------- Paths ------------------------------------ # # Path to directory where to store the data (separate multiple locations by comma): # #path.data: /path/to/data # # Path to log files: # #path.logs: /path/to/logs # # ----------------------------------- Memory ----------------------------------- # # Lock the memory on startup: # #bootstrap.memory_lock: true # # Make sure that the heap size is set to about half the memory available # on the system and that the owner of the process is allowed to use this # limit. # # Elasticsearch performs poorly when the system is swapping the memory. # # ---------------------------------- Network ----------------------------------- # # Set the bind address to a specific IP (IPv4 or IPv6): # network.host: [_eth0_, _local_] # # Set a custom port for HTTP: # #http.port: 9200 # # For more information, consult the network module documentation. # # --------------------------------- Discovery ---------------------------------- # # Pass an initial list of hosts to perform discovery when new node is started: # The default list of hosts is ["127.0.0.1", "[::1]"] # discovery.zen.ping.unicast.hosts: ["$elasticvm1ip", "$elasticvm2ip", "$elasticvm3ip"] # # Prevent the "split brain" by configuring the majority of nodes (total number of master-eligible nodes / 2 + 1): # discovery.zen.minimum_master_nodes: 3 # # For more information, consult the zen discovery module documentation. # # ---------------------------------- Gateway ----------------------------------- # # Block initial recovery after a full cluster restart until N nodes are started: # #gateway.recover_after_nodes: 3 # # For more information, consult the gateway module documentation. # # ---------------------------------- Various ----------------------------------- # # Require explicit names when deleting indices: # #action.destructive_requires_name: true EOF service elasticsearch restart } > /tmp/setup.log ================================================ FILE: scripts/install_gluster.sh ================================================ #!/bin/bash # This script built for Ubuntu Server 16.04 LTS # You can customize variables such as MOUNTPOINT, RAIDCHUNKSIZE and so on to your needs. # You can also customize it to work with other Linux flavours and versions. # If you customize it, copy it to either Azure blob storage or Github so that Azure # custom script Linux VM extension can access it, and specify its location in the # parameters of powershell script or runbook or Azure Resource Manager CRP template. . ./helper_functions.sh AZUREVMOFFSET=4 NODENAME=$(hostname) PEERNODEPREFIX=${1} PEERNODEIPPREFIX=${2} VOLUMENAME=${3} NODEINDEX=${4} NODECOUNT=${5} echo $NODENAME >> /tmp/vars.txt echo $PEERNODEPREFIX >> /tmp/vars.txt echo $PEERNODEIPPREFIX >> /tmp/vars.txt echo $VOLUMENAME >> /tmp/vars.txt echo $NODEINDEX >> /tmp/vars.txt echo $NODECOUNT >> /tmp/vars.txt MOUNTPOINT="/datadrive" RAIDCHUNKSIZE=128 RAIDDISK="/dev/md1" RAIDPARTITION="/dev/md1p1" # An set of disks to ignore from partitioning and formatting BLACKLIST="/dev/sda|/dev/sdb" # make sure the system does automatic update apt_update_noninteractive apt_install_noninteractive unattended-upgrades { check_os() { grep -q -s ubuntu /proc/version && _RET=$? || _RET=$? isubuntu=$_RET } scan_for_new_disks() { # Looks for unpartitioned disks declare -a RET DEVS=($(ls -1 /dev/sd*|egrep -v "${BLACKLIST}"|egrep -v "[0-9]$")) for DEV in "${DEVS[@]}"; do # Check each device if there is a "1" partition. If not, # "assume" it is not partitioned. if [ ! -b ${DEV}1 ]; then RET+="${DEV} " fi done echo "${RET}" } get_disk_count() { DISKCOUNT=0 for DISK in "${DISKS[@]}"; do DISKCOUNT+=1 done; echo "$DISKCOUNT" } create_raid0_ubuntu() { dpkg -s mdadm && _RET=$? || _RET=$? if [ $_RET -eq 1 ]; then echo "installing mdadm" sudo apt-get -y -q install mdadm fi echo "Creating raid0" udevadm control --stop-exec-queue echo "yes" | mdadm --create "$RAIDDISK" --name=data --level=0 --chunk="$RAIDCHUNKSIZE" --raid-devices="$DISKCOUNT" "${DISKS[@]}" udevadm control --start-exec-queue mdadm --detail --verbose --scan > /etc/mdadm.conf } do_partition() { # This function creates one (1) primary partition on the # disk, using all available space DISK=${1} echo "Partitioning disk $DISK" echo -ne "n\np\n1\n\n\nw\n" | fdisk "${DISK}" #> /dev/null 2>&1 # # Use the bash-specific $PIPESTATUS to ensure we get the correct exit code # from fdisk and not from echo if [ ${PIPESTATUS[1]} -ne 0 ]; then echo "An error occurred partitioning ${DISK}" >&2 echo "I cannot continue" >&2 exit 2 fi } add_to_fstab() { UUID=${1} MOUNTPOINT=${2} grep -q -s "${UUID}" /etc/fstab && _RET=$? || _RET=$? if [ $_RET -eq 0 ]; then echo "Not adding ${UUID} to fstab again (it's already there!)" else LINE="UUID=${UUID} ${MOUNTPOINT} ext4 defaults,noatime 0 0" echo -e "${LINE}" >> /etc/fstab fi } configure_disks() { ls "${MOUNTPOINT}" && _RET=$? || _RET=$? if [ $_RET -eq 0 ] then return fi DISKS=($(scan_for_new_disks)) echo "Disks are ${DISKS[@]}" declare -i DISKCOUNT DISKCOUNT=$(get_disk_count) echo "Disk count is $DISKCOUNT" if [ $DISKCOUNT -gt 1 ]; then create_raid0_ubuntu do_partition ${RAIDDISK} PARTITION="${RAIDPARTITION}" else DISK="${DISKS[0]}" do_partition ${DISK} PARTITION=$(fdisk -l ${DISK}|grep -A 1 Device|tail -n 1|awk '{print $1}') fi echo "Creating filesystem on ${PARTITION}." mkfs -t ext4 ${PARTITION} mkdir "${MOUNTPOINT}" read UUID FS_TYPE < <(blkid -u filesystem ${PARTITION}|awk -F "[= ]" '{print $3" "$5}'|tr -d "\"") add_to_fstab "${UUID}" "${MOUNTPOINT}" echo "Mounting disk ${PARTITION} on ${MOUNTPOINT}" mount "${MOUNTPOINT}" } open_ports() { index=0 while [ $index -lt $NODECOUNT ]; do echo "Node ${index}" thisNode="${PEERNODEIPPREFIX}.$(($index+$AZUREVMOFFSET))" echo "Node ${thisNode}" if [ $index -ne $NODEINDEX ]; then echo "Node ${thisNode} is a peer" iptables -I INPUT -p all -s "${thisNode}" -j ACCEPT echo "${thisNode} ${thisNode}" >> /etc/hosts else echo "Node ${thisNode} is me" echo "127.0.0.1 ${thisNode}" >> /etc/hosts fi let index++ done iptables-save } disable_apparmor_ubuntu() { /etc/init.d/apparmor teardown update-rc.d -f apparmor remove } configure_network() { open_ports disable_apparmor_ubuntu } install_glusterfs_ubuntu() { dpkg -l | grep glusterfs && _RET=$? || _RET=$? if [ $_RET -eq 0 ]; then return fi if [ ! -e /etc/apt/sources.list.d/gluster* ]; then echo "adding gluster ppa" apt_install_noninteractive python-software-properties apt-add-repository ppa:gluster/glusterfs-3.10 --yes apt_update_noninteractive fi echo "installing gluster" apt_install_noninteractive glusterfs-server return } configure_gluster() { echo "gluster step1" if [ $isubuntu -eq 0 ]; then /etc/init.d/glusterfs-server status && _RET=$? || _RET=$? if [ $_RET -ne 0 ]; then install_glusterfs_ubuntu fi /etc/init.d/glusterfs-server start fi echo "gluster step2" GLUSTERDIR="${MOUNTPOINT}/brick" ls "${GLUSTERDIR}" && _RET=$? || _RET=$? if [ $_RET -ne 0 ]; then mkdir "${GLUSTERDIR}" fi if [ $NODEINDEX -lt $(($NODECOUNT-1)) ]; then return fi echo "gluster step3" allNodes="${NODENAME}:${GLUSTERDIR}" echo $allNodes retry=10 failed=1 while [ $retry -gt 0 ] && [ $failed -gt 0 ]; do failed=0 index=0 echo retrying $retry while [ $index -lt $(($NODECOUNT-1)) ]; do glustervm=${PEERNODEPREFIX}${index} echo $glustervm ping -c 3 $glustervm gluster peer probe $glustervm && _RET=$? || _RET=$? if [ $_RET -ne 0 ]; then failed=1 echo "gluster peer probe $glustervm failed" fi gluster peer status gluster peer status | grep $glustervm && _RET=$? || _RET=$? if [ $_RET -ne 0 ]; then failed=1 echo "gluster peer status $glustervm failed" fi if [ $retry -eq 10 ]; then allNodes="${allNodes} $glustervm:${GLUSTERDIR}" fi let index++ done sleep 30 let retry-- done echo "gluster step4" echo $allnodes sleep 60 gluster volume create ${VOLUMENAME} rep 2 transport tcp ${allNodes} gluster volume info gluster volume start ${VOLUMENAME} echo "gluster complete" } # "main routine" check_os configure_network configure_disks configure_gluster } > /tmp/gluster-setup.log ================================================ FILE: scripts/install_moodle.sh ================================================ #!/bin/bash # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. set -ex #parameters { moodle_on_azure_configs_json_path=${1} . ./helper_functions.sh get_setup_params_from_configs_json $moodle_on_azure_configs_json_path || exit 99 echo $moodleVersion >> /tmp/vars.txt echo $glusterNode >> /tmp/vars.txt echo $glusterVolume >> /tmp/vars.txt echo $siteFQDN >> /tmp/vars.txt echo $httpsTermination >> /tmp/vars.txt echo $dbIP >> /tmp/vars.txt echo $moodledbname >> /tmp/vars.txt echo $moodledbuser >> /tmp/vars.txt echo $moodledbpass >> /tmp/vars.txt echo $adminpass >> /tmp/vars.txt echo $dbadminlogin >> /tmp/vars.txt echo $dbadminloginazure >> /tmp/vars.txt echo $dbadminpass >> /tmp/vars.txt echo $storageAccountName >> /tmp/vars.txt echo $storageAccountKey >> /tmp/vars.txt echo $azuremoodledbuser >> /tmp/vars.txt echo $redisDns >> /tmp/vars.txt echo $redisAuth >> /tmp/vars.txt echo $elasticVm1IP >> /tmp/vars.txt echo $installO365pluginsSwitch >> /tmp/vars.txt echo $dbServerType >> /tmp/vars.txt echo $fileServerType >> /tmp/vars.txt echo $mssqlDbServiceObjectiveName >> /tmp/vars.txt echo $mssqlDbEdition >> /tmp/vars.txt echo $mssqlDbSize >> /tmp/vars.txt echo $installObjectFsSwitch >> /tmp/vars.txt echo $installGdprPluginsSwitch >> /tmp/vars.txt echo $thumbprintSslCert >> /tmp/vars.txt echo $thumbprintCaCert >> /tmp/vars.txt echo $searchType >> /tmp/vars.txt echo $azureSearchKey >> /tmp/vars.txt echo $azureSearchNameHost >> /tmp/vars.txt echo $tikaVmIP >> /tmp/vars.txt echo $nfsByoIpExportPath >> /tmp/vars.txt echo $storageAccountType >>/tmp/vars.txt echo $fileServerDiskSize >>/tmp/vars.txt echo $phpVersion >> /tmp/vars.txt echo $isMigration >> /tmp/vars.txt check_fileServerType_param $fileServerType if [ "$dbServerType" = "mysql" ]; then mysqlIP=$dbIP mysqladminlogin=$dbadminloginazure mysqladminpass=$dbadminpass elif [ "$dbServerType" = "mssql" ]; then mssqlIP=$dbIP mssqladminlogin=$dbadminloginazure mssqladminpass=$dbadminpass elif [ "$dbServerType" = "postgres" ]; then postgresIP=$dbIP pgadminlogin=$dbadminloginazure pgadminpass=$dbadminpass else echo "Invalid dbServerType ($dbServerType) given. Only 'mysql' or 'postgres' or 'mssql' is allowed. Exiting" exit 1 fi # # Export apt default settings for this install script # apt_update_noninteractive >> /tmp/apt.log apt_install_noninteractive fail2ban >> /tmp/apt.log config_fail2ban # create gluster, nfs or Azure Files mount point mkdir -p /moodle if [ $fileServerType = "gluster" ]; then # configure gluster repository & install gluster clientapt add-apt-repository ppa:gluster/glusterfs-9 --yes >> /tmp/apt.log elif [ $fileServerType = "nfs" ]; then # configure NFS server and export setup_raid_disk_and_filesystem /moodle /dev/md1 /dev/md1p1 configure_nfs_server_and_export /moodle fi apt_update_noninteractive >> /tmp/apt.log apt_install_noninteractive rsyslog git >> /tmp/apt.log if [ $fileServerType = "gluster" ]; then apt_install_noninteractive glusterfs-client >> /tmp/apt.log elif [ "$fileServerType" = "azurefiles" ]; then apt_install_noninteractive \ linux-modules-extra-azure \ cifs-utils >> /tmp/apt.log # Because of https://bugs.launchpad.net/ubuntu/+source/linux-azure/+bug/2042092 modprobe cifs && true fi if [ $dbServerType = "mysql" ]; then apt_install_noninteractive mysql-client >> /tmp/apt.log elif [ "$dbServerType" = "postgres" ]; then apt_install_noninteractive postgresql-client >> /tmp/apt.log fi if [ "$installObjectFsSwitch" = "true" -o "$fileServerType" = "azurefiles" ]; then # install azure cli AZ_REPO=$(lsb_release -cs) mkdir -p /etc/apt/keyrings curl -sLS https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/keyrings/microsoft.gpg && chmod go+r /etc/apt/keyrings/microsoft.gpg echo "deb [arch=`dpkg --print-architecture` signed-by=/etc/apt/keyrings/microsoft.gpg] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" > /etc/apt/sources.list.d/azure-cli.list apt_update_noninteractive >> /tmp/apt.log apt_install_noninteractive apt-transport-https ca-certificates curl apt-transport-https lsb-release gnupg azure-cli >> /tmp/apt.log # FileStorage accounts can only be used to store Azure file shares; # Premium_LRS will support FileStorage kind # No other storage resources (blob containers, queues, tables, etc.) can be deployed in a FileStorage account. if [ $storageAccountType != "Premium_LRS" ]; then az storage container create \ --name objectfs \ --account-name $storageAccountName \ --account-key $storageAccountKey \ --public-access off \ --fail-on-exist >> /tmp/wabs.log az storage container policy create \ --account-name $storageAccountName \ --account-key $storageAccountKey \ --container-name objectfs \ --name readwrite \ --start $(date --date="1 day ago" +%F) \ --expiry $(date --date="2199-01-01" +%F) \ --permissions rw >> /tmp/wabs.log sas=$(az storage container generate-sas \ --account-name $storageAccountName \ --account-key $storageAccountKey \ --name objectfs \ --policy readwrite \ --output tsv) fi # If its a migration flow, then mount the azure file share now. if [ "$isMigration" = "true" ]; then # On migration flow, the moodle azure file share must present before running this script. echo -e '\n\rIts a migration flow, check whether moodle fileshare exists\n\r' check_azure_files_moodle_share_exists $storageAccountName $storageAccountKey # Set up and mount Azure Files share. echo -e '\n\rSetting up and mounting Azure Files share //'$storageAccountName'.file.core.windows.net/moodle on /moodle\n\r' setup_and_mount_azure_files_moodle_share $storageAccountName $storageAccountKey fi fi if [ $fileServerType = "gluster" ]; then # mount gluster files system echo -e '\n\rInstalling GlusterFS on '$glusterNode':/'$glusterVolume '/moodle\n\r' setup_and_mount_gluster_moodle_share $glusterNode $glusterVolume elif [ $fileServerType = "nfs-ha" ]; then # mount NFS-HA export echo -e '\n\rMounting NFS export from '$nfsHaLbIP' on /moodle\n\r' configure_nfs_client_and_mount $nfsHaLbIP $nfsHaExportPath /moodle elif [ $fileServerType = "nfs-byo" ]; then # mount NFS-BYO export echo -e '\n\rMounting NFS export from '$nfsByoIpExportPath' on /moodle\n\r' configure_nfs_client_and_mount0 $nfsByoIpExportPath /moodle fi # install pre-requisites add-apt-repository ppa:ubuntu-toolchain-r/ppa --yes >> /tmp/apt.log apt_update_noninteractive >> /tmp/apt.log apt_install_noninteractive software-properties-common unzip >> /tmp/apt.log # install the entire stack apt_install_noninteractive \ nginx \ varnish \ php \ php-fpm \ php-cli \ php-curl \ php-zip \ graphviz \ aspell \ php-common \ php-soap \ php-json \ php-redis \ php-bcmath \ php-ldap \ php-gd \ php-xmlrpc \ php-intl \ php-xml \ php-bz2 \ php-pear \ php-mbstring \ php-dev \ mcrypt >> /tmp/apt.log PhpVer=$(get_php_version) if [ $dbServerType = "mysql" ]; then apt_install_noninteractive php-mysql elif [ $dbServerType = "mssql" ]; then apt_install_noninteractive libapache2-mod-php install_php_mssql_driver else apt_install_noninteractive php-pgsql fi # Set up initial moodle dirs mkdir -p /moodle/html mkdir -p /moodle/certs mkdir -p /moodle/moodledata o365pluginVersion=$(get_o365plugin_version_from_moodle_version $moodleVersion) moodleStableVersion=$o365pluginVersion # Need Moodle stable version for GDPR plugins, and o365pluginVersion is just Moodle stable version, so reuse it. moodleUnzipDir=$(get_moodle_unzip_dir_from_moodle_version $moodleVersion) # install Moodle cat < /tmp/setup-moodle.sh #!/bin/bash mkdir -p /moodle/tmp cd /moodle/tmp if [ ! -d /moodle/html/moodle ]; then # downloading moodle only if /moodle/html/moodle does not exist -- if it exists, user should populate it in advance correctly as below. This is to reduce template deployment time. /usr/bin/curl -k --max-redirs 10 https://github.com/moodle/moodle/archive/MOODLE_405_STABLE.zip -L -o moodle.zip /usr/bin/unzip -q moodle.zip /bin/mv "$moodleUnzipDir" /moodle/html/moodle fi if [ "$installGdprPluginsSwitch" = "true" ]; then # install Moodle GDPR plugins (Note: This is only for Moodle versions 3.4.2+ or 3.3.5+ and will be included in Moodle 3.5, so no need for 3.5) curl -k --max-redirs 10 https://github.com/moodlehq/moodle-tool_policy/archive/"$moodleStableVersion".zip -L -o plugin-policy.zip unzip -q plugin-policy.zip mv moodle-tool_policy-"$moodleStableVersion" /moodle/html/moodle/admin/tool/policy curl -k --max-redirs 10 https://github.com/moodlehq/moodle-tool_dataprivacy/archive/"$moodleStableVersion".zip -L -o plugin-dataprivacy.zip unzip -q plugin-dataprivacy.zip mv moodle-tool_dataprivacy-"$moodleStableVersion" /moodle/html/moodle/admin/tool/dataprivacy fi if [ "$installO365pluginsSwitch" = "true" ]; then # install Office 365 plugins curl -k --max-redirs 10 https://github.com/Microsoft/o365-moodle/archive/"$o365pluginVersion".zip -L -o o365.zip unzip -q o365.zip cp -r o365-moodle-"$o365pluginVersion"/* /moodle/html/moodle rm -rf o365-moodle-"$o365pluginVersion" fi if [ "$searchType" = "elastic" ]; then # Install ElasticSearch plugin /usr/bin/curl -k --max-redirs 10 https://github.com/catalyst/moodle-search_elastic/archive/master.zip -L -o plugin-elastic.zip /usr/bin/unzip -q plugin-elastic.zip /bin/mv moodle-search_elastic-master /moodle/html/moodle/search/engine/elastic # Install ElasticSearch plugin dependency /usr/bin/curl -k --max-redirs 10 https://github.com/catalyst/moodle-local_aws/archive/master.zip -L -o local-aws.zip /usr/bin/unzip -q local-aws.zip /bin/mv moodle-local_aws-master /moodle/html/moodle/local/aws elif [ "$searchType" = "azure" ]; then # Install Azure Search service plugin /usr/bin/curl -k --max-redirs 10 https://github.com/catalyst/moodle-search_azure/archive/master.zip -L -o plugin-azure-search.zip /usr/bin/unzip -q plugin-azure-search.zip /bin/mv moodle-search_azure-master /moodle/html/moodle/search/engine/azure fi if [ "$installObjectFsSwitch" = "true" ]; then # Install the ObjectFS plugin /usr/bin/curl -k --max-redirs 10 https://github.com/catalyst/moodle-tool_objectfs/archive/master.zip -L -o plugin-objectfs.zip /usr/bin/unzip -q plugin-objectfs.zip /bin/mv moodle-tool_objectfs-master /moodle/html/moodle/admin/tool/objectfs # Install the ObjectFS Azure library /usr/bin/curl -k --max-redirs 10 https://github.com/catalyst/moodle-local_azure_storage/archive/master.zip -L -o plugin-azurelibrary.zip /usr/bin/unzip -q plugin-azurelibrary.zip /bin/mv moodle-local_azure_storage-master /moodle/html/moodle/local/azure_storage fi cd /moodle rm -rf /moodle/tmp EOF chmod 755 /tmp/setup-moodle.sh /tmp/setup-moodle.sh >> /tmp/setupmoodle.log # Build nginx config cat < /etc/nginx/nginx.conf user www-data; worker_processes 2; pid /run/nginx.pid; events { worker_connections 768; } http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; client_max_body_size 0; proxy_max_temp_file_size 0; server_names_hash_bucket_size 128; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; proxy_buffering off; include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; set_real_ip_from 127.0.0.1; real_ip_header X-Forwarded-For; #upgrading to TLSv1.2 and droping 1 & 1.1 ssl_protocols TLSv1.2; #ssl_prefer_server_ciphers on; #adding ssl ciphers ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384; gzip on; gzip_disable "msie6"; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; EOF if [ "$httpsTermination" != "None" ]; then cat <> /etc/nginx/nginx.conf map \$http_x_forwarded_proto \$fastcgi_https { default \$https; http ''; https on; } EOF fi cat <> /etc/nginx/nginx.conf log_format moodle_combined '\$remote_addr - \$upstream_http_x_moodleuser [\$time_local] ' '"\$request" \$status \$body_bytes_sent ' '"\$http_referer" "\$http_user_agent"'; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; } EOF cat <> /etc/nginx/sites-enabled/${siteFQDN}.conf server { listen 81 default; server_name ${siteFQDN}; root /moodle/html/moodle; index index.php index.html index.htm; # Log to syslog error_log syslog:server=localhost,facility=local1,severity=error,tag=moodle; access_log syslog:server=localhost,facility=local1,severity=notice,tag=moodle moodle_combined; # Log XFF IP instead of varnish set_real_ip_from 10.0.0.0/8; set_real_ip_from 127.0.0.1; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; real_ip_header X-Forwarded-For; real_ip_recursive on; EOF if [ "$httpsTermination" != "None" ]; then cat <> /etc/nginx/sites-enabled/${siteFQDN}.conf # Redirect to https if (\$http_x_forwarded_proto != https) { return 301 https://\$server_name\$request_uri; } rewrite ^/(.*\.php)(/)(.*)$ /\$1?file=/\$3 last; EOF fi cat <> /etc/nginx/sites-enabled/${siteFQDN}.conf # Filter out php-fpm status page location ~ ^/server-status { return 404; } location / { try_files \$uri \$uri/index.php?\$query_string; } location ~ [^/]\.php(/|$) { fastcgi_split_path_info ^(.+?\.php)(/.*)$; if (!-f \$document_root\$fastcgi_script_name) { return 404; } fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; fastcgi_pass unix:/run/php/php${PhpVer}-fpm.sock; fastcgi_read_timeout 3600; fastcgi_index index.php; include fastcgi_params; } } EOF if [ "$httpsTermination" = "VMSS" ]; then cat <> /etc/nginx/sites-enabled/${siteFQDN}.conf server { listen 443 ssl; root /moodle/html/moodle; index index.php index.html index.htm; ssl on; ssl_certificate /moodle/certs/nginx.crt; ssl_certificate_key /moodle/certs/nginx.key; # Log to syslog error_log syslog:server=localhost,facility=local1,severity=error,tag=moodle; access_log syslog:server=localhost,facility=local1,severity=notice,tag=moodle moodle_combined; # Log XFF IP instead of varnish set_real_ip_from 10.0.0.0/8; set_real_ip_from 127.0.0.1; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; real_ip_header X-Forwarded-For; real_ip_recursive on; location / { proxy_set_header Host \$host; proxy_set_header HTTP_REFERER \$http_referer; proxy_set_header X-Forwarded-Host \$host; proxy_set_header X-Forwarded-Server \$host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_pass http://localhost:80; } } EOF fi if [ "$httpsTermination" = "VMSS" ]; then ### SSL cert ### if [ "$thumbprintSslCert" != "None" ]; then echo "Using VM's cert (/var/lib/waagent/$thumbprintSslCert.*) for SSL..." cat /var/lib/waagent/$thumbprintSslCert.prv > /moodle/certs/nginx.key cat /var/lib/waagent/$thumbprintSslCert.crt > /moodle/certs/nginx.crt if [ "$thumbprintCaCert" != "None" ]; then echo "CA cert was specified (/var/lib/waagent/$thumbprintCaCert.crt), so append it to nginx.crt..." cat /var/lib/waagent/$thumbprintCaCert.crt >> /moodle/certs/nginx.crt fi else echo -e "Generating SSL self-signed certificate" openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /moodle/certs/nginx.key -out /moodle/certs/nginx.crt -subj "/C=US/ST=WA/L=Redmond/O=IT/CN=$siteFQDN" fi chown www-data:www-data /moodle/certs/nginx.* chmod 0400 /moodle/certs/nginx.* fi # PHP 8 fpm config PhpVer=$(get_php_version) PhpIni=/etc/php/${PhpVer}/fpm/php.ini sed -i "s/memory_limit.*/memory_limit = 512M/" $PhpIni sed -i "s/max_execution_time.*/max_execution_time = 18000/" $PhpIni sed -i "s/;max_input_vars.*/max_input_vars = 100000/" $PhpIni sed -i "s/max_input_time.*/max_input_time = 600/" $PhpIni sed -i "s/upload_max_filesize.*/upload_max_filesize = 1024M/" $PhpIni sed -i "s/post_max_size.*/post_max_size = 1056M/" $PhpIni sed -i "s/;opcache.use_cwd.*/opcache.use_cwd = 1/" $PhpIni sed -i "s/;opcache.validate_timestamps.*/opcache.validate_timestamps = 1/" $PhpIni sed -i "s/;opcache.save_comments.*/opcache.save_comments = 1/" $PhpIni sed -i "s/;opcache.enable_file_override.*/opcache.enable_file_override = 0/" $PhpIni sed -i "s/;opcache.enable.*/opcache.enable = 1/" $PhpIni sed -i "s/;opcache.memory_consumption.*/opcache.memory_consumption = 256/" $PhpIni sed -i "s/;opcache.max_accelerated_files.*/opcache.max_accelerated_files = 8000/" $PhpIni # required for PHP8 cli at install time PhpIniCli=/etc/php/${PhpVer}/cli/php.ini sed -i "s/;max_input_vars.*/max_input_vars = 100000/" $PhpIniCli # fpm config - overload this cat < /etc/php/${PhpVer}/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /run/php/php${PhpVer}-fpm.sock listen.owner = www-data listen.group = www-data pm = dynamic pm.max_children = 3000 pm.start_servers = 20 pm.min_spare_servers = 22 pm.max_spare_servers = 30 EOF # Remove the default site. Moodle is the only site we want rm -f /etc/nginx/sites-enabled/default # restart Nginx sudo service nginx restart # Configure varnish startup for 16.04 VARNISHSTART="ExecStart=\/usr\/sbin\/varnishd -j unix,user=vcache -F -a :80 -T localhost:6082 -f \/etc\/varnish\/moodle.vcl -S \/etc\/varnish\/secret -s malloc,1024m -p thread_pool_min=200 -p thread_pool_max=4000 -p thread_pool_add_delay=2 -p timeout_linger=100 -p timeout_idle=30 -p send_timeout=1800 -p thread_pools=4 -p http_max_hdr=512 -p workspace_backend=512k" sed -i "s/^ExecStart.*/${VARNISHSTART}/" /lib/systemd/system/varnish.service # Configure varnish VCL for moodle cat <> /etc/varnish/moodle.vcl vcl 4.0; import std; import directors; backend default { .host = "localhost"; .port = "81"; .first_byte_timeout = 3600s; .connect_timeout = 600s; .between_bytes_timeout = 600s; } sub vcl_recv { # Varnish does not support SPDY or HTTP/2.0 untill we upgrade to Varnish 5.0 if (req.method == "PRI") { return (synth(405)); } if (req.restarts == 0) { if (req.http.X-Forwarded-For) { set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip; } else { set req.http.X-Forwarded-For = client.ip; } } # Non-RFC2616 or CONNECT HTTP requests methods filtered. Pipe requests directly to backend if (req.method != "GET" && req.method != "HEAD" && req.method != "PUT" && req.method != "POST" && req.method != "TRACE" && req.method != "OPTIONS" && req.method != "DELETE") { return (pipe); } # Varnish don't mess with healthchecks if (req.url ~ "^/admin/tool/heartbeat" || req.url ~ "^/healthcheck.php") { return (pass); } # Pipe requests to backup.php straight to backend - prevents problem with progress bar long polling 503 problem # This is here because backup.php is POSTing to itself - Filter before !GET&&!HEAD if (req.url ~ "^/backup/backup.php") { return (pipe); } # Varnish only deals with GET and HEAD by default. If request method is not GET or HEAD, pass request to backend if (req.method != "GET" && req.method != "HEAD") { return (pass); } ### Rules for Moodle and Totara sites ### # Moodle doesn't require Cookie to serve following assets. Remove Cookie header from request, so it will be looked up. if ( req.url ~ "^/altlogin/.+/.+\.(png|jpg|jpeg|gif|css|js|webp)$" || req.url ~ "^/pix/.+\.(png|jpg|jpeg|gif)$" || req.url ~ "^/theme/font.php" || req.url ~ "^/theme/image.php" || req.url ~ "^/theme/javascript.php" || req.url ~ "^/theme/jquery.php" || req.url ~ "^/theme/styles.php" || req.url ~ "^/theme/yui" || req.url ~ "^/lib/javascript.php/-1/" || req.url ~ "^/lib/requirejs.php/-1/" ) { set req.http.X-Long-TTL = "86400"; unset req.http.Cookie; return(hash); } # Perform lookup for selected assets that we know are static but Moodle still needs a Cookie if( req.url ~ "^/theme/.+\.(png|jpg|jpeg|gif|css|js|webp)" || req.url ~ "^/lib/.+\.(png|jpg|jpeg|gif|css|js|webp)" || req.url ~ "^/pluginfile.php/[0-9]+/course/overviewfiles/.+\.(?i)(png|jpg)$" ) { # Set internal temporary header, based on which we will do things in vcl_backend_response set req.http.X-Long-TTL = "86400"; return (hash); } # Serve requests to SCORM checknet.txt from varnish. Have to remove get parameters. Response body always contains "1" if ( req.url ~ "^/lib/yui/build/moodle-core-checknet/assets/checknet.txt" ) { set req.url = regsub(req.url, "(.*)\?.*", "\1"); unset req.http.Cookie; # Will go to hash anyway at the end of vcl_recv set req.http.X-Long-TTL = "86400"; return(hash); } # Requests containing "Cookie" or "Authorization" headers will not be cached if (req.http.Authorization || req.http.Cookie) { return (pass); } # Almost everything in Moodle correctly serves Cache-Control headers, if # needed, which varnish will honor, but there are some which don't. Rather # than explicitly finding them all and listing them here we just fail safe # and don't cache unknown urls that get this far. return (pass); } sub vcl_backend_response { # Happens after we have read the response headers from the backend. # # Here you clean the response headers, removing silly Set-Cookie headers # and other mistakes your backend does. # We know these assest are static, let's set TTL >0 and allow client caching if ( beresp.http.Cache-Control && bereq.http.X-Long-TTL && beresp.ttl < std.duration(bereq.http.X-Long-TTL + "s", 1s) && !beresp.http.WWW-Authenticate ) { # If max-age < defined in X-Long-TTL header set beresp.http.X-Orig-Pragma = beresp.http.Pragma; unset beresp.http.Pragma; set beresp.http.X-Orig-Cache-Control = beresp.http.Cache-Control; set beresp.http.Cache-Control = "public, max-age="+bereq.http.X-Long-TTL+", no-transform"; set beresp.ttl = std.duration(bereq.http.X-Long-TTL + "s", 1s); unset bereq.http.X-Long-TTL; } else if( !beresp.http.Cache-Control && bereq.http.X-Long-TTL && !beresp.http.WWW-Authenticate ) { set beresp.http.X-Orig-Pragma = beresp.http.Pragma; unset beresp.http.Pragma; set beresp.http.Cache-Control = "public, max-age="+bereq.http.X-Long-TTL+", no-transform"; set beresp.ttl = std.duration(bereq.http.X-Long-TTL + "s", 1s); unset bereq.http.X-Long-TTL; } else { # Don't touch headers if max-age > defined in X-Long-TTL header unset bereq.http.X-Long-TTL; } # Here we set X-Trace header, prepending it to X-Trace header received from backend. Useful for troubleshooting if(beresp.http.x-trace && !beresp.was_304) { set beresp.http.X-Trace = regsub(server.identity, "^([^.]+),?.*$", "\1")+"->"+regsub(beresp.backend.name, "^(.+)\((?:[0-9]{1,3}\.){3}([0-9]{1,3})\)","\1(\2)")+"->"+beresp.http.X-Trace; } else { set beresp.http.X-Trace = regsub(server.identity, "^([^.]+),?.*$", "\1")+"->"+regsub(beresp.backend.name, "^(.+)\((?:[0-9]{1,3}\.){3}([0-9]{1,3})\)","\1(\2)"); } # Gzip JS, CSS is done at the ngnix level doing it here dosen't respect the no buffer requsets # if (beresp.http.content-type ~ "application/javascript.*" || beresp.http.content-type ~ "text") { # set beresp.do_gzip = true; #} } sub vcl_deliver { # Revert back to original Cache-Control header before delivery to client if (resp.http.X-Orig-Cache-Control) { set resp.http.Cache-Control = resp.http.X-Orig-Cache-Control; unset resp.http.X-Orig-Cache-Control; } # Revert back to original Pragma header before delivery to client if (resp.http.X-Orig-Pragma) { set resp.http.Pragma = resp.http.X-Orig-Pragma; unset resp.http.X-Orig-Pragma; } # (Optional) X-Cache HTTP header will be added to responce, indicating whether object was retrieved from backend, or served from cache if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; } else { set resp.http.X-Cache = "MISS"; } # Set X-AuthOK header when totara/varnsih authentication succeeded if (req.http.X-AuthOK) { set resp.http.X-AuthOK = req.http.X-AuthOK; } # If desired "Via: 1.1 Varnish-v4" response header can be removed from response unset resp.http.Via; unset resp.http.Server; return(deliver); } sub vcl_backend_error { # More comprehensive varnish error page. Display time, instance hostname, host header, url for easier troubleshooting. set beresp.http.Content-Type = "text/html; charset=utf-8"; set beresp.http.Retry-After = "5"; synthetic( {" "} + beresp.status + " " + beresp.reason + {"

Error "} + beresp.status + " " + beresp.reason + {"

"} + beresp.reason + {"

Guru Meditation:

Time: "} + now + {"

Node: "} + server.hostname + {"

Host: "} + bereq.http.host + {"

URL: "} + bereq.url + {"

XID: "} + bereq.xid + {"


Varnish cache server "} ); return (deliver); } sub vcl_synth { #Redirect using '301 - Permanent Redirect', permanent redirect if (resp.status == 851) { set resp.http.Location = req.http.x-redir; set resp.http.X-Varnish-Redirect = true; set resp.status = 301; return (deliver); } #Redirect using '302 - Found', temporary redirect if (resp.status == 852) { set resp.http.Location = req.http.x-redir; set resp.http.X-Varnish-Redirect = true; set resp.status = 302; return (deliver); } #Redirect using '307 - Temporary Redirect', !GET&&!HEAD requests, dont change method on redirected requests if (resp.status == 857) { set resp.http.Location = req.http.x-redir; set resp.http.X-Varnish-Redirect = true; set resp.status = 307; return (deliver); } #Respond with 403 - Forbidden if (resp.status == 863) { set resp.http.X-Varnish-Error = true; set resp.status = 403; return (deliver); } } EOF # Restart Varnish systemctl daemon-reload service varnish restart if [ $dbServerType = "mysql" ]; then mysql -h $mysqlIP -u $mysqladminlogin -p${mysqladminpass} -e "CREATE DATABASE ${moodledbname} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" | tee -a /tmp/debug mysql -h $mysqlIP -u $mysqladminlogin -p${mysqladminpass} -e "CREATE USER ${moodledbuser}@'%' IDENTIFIED BY '${moodledbpass}';" | tee -a /tmp/debug mysql -h $mysqlIP -u $mysqladminlogin -p${mysqladminpass} -e "GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,CREATE TEMPORARY TABLES,DROP,INDEX,ALTER ON ${moodledbname}.* TO ${moodledbuser}@'%';" | tee -a /tmp/debug elif [ $dbServerType = "mssql" ]; then /opt/mssql-tools/bin/sqlcmd -S $mssqlIP -U $mssqladminlogin -P ${mssqladminpass} -Q "CREATE DATABASE ${moodledbname} ( MAXSIZE = $mssqlDbSize, EDITION = '$mssqlDbEdition', SERVICE_OBJECTIVE = '$mssqlDbServiceObjectiveName' )" /opt/mssql-tools/bin/sqlcmd -S $mssqlIP -U $mssqladminlogin -P ${mssqladminpass} -Q "CREATE LOGIN ${moodledbuser} with password = '${moodledbpass}'" /opt/mssql-tools/bin/sqlcmd -S $mssqlIP -U $mssqladminlogin -P ${mssqladminpass} -d ${moodledbname} -Q "CREATE USER ${moodledbuser} FROM LOGIN ${moodledbuser}" /opt/mssql-tools/bin/sqlcmd -S $mssqlIP -U $mssqladminlogin -P ${mssqladminpass} -d ${moodledbname} -Q "exec sp_addrolemember 'db_owner','${moodledbuser}'" else # Create postgres db echo "${postgresIP}:5432:postgres:${pgadminlogin}:${pgadminpass}" > /root/.pgpass chmod 600 /root/.pgpass psql -h $postgresIP -U $pgadminlogin -c "CREATE DATABASE ${moodledbname};" postgres psql -h $postgresIP -U $pgadminlogin -c "CREATE USER ${moodledbuser} WITH PASSWORD '${moodledbpass}';" postgres psql -h $postgresIP -U $pgadminlogin -c "GRANT ALL ON DATABASE ${moodledbname} TO ${moodledbuser};" postgres rm -f /root/.pgpass fi # Master config for syslog mkdir /var/log/sitelogs chown syslog.adm /var/log/sitelogs cat <> /etc/rsyslog.conf \$ModLoad imudp \$UDPServerRun 514 EOF cat <> /etc/rsyslog.d/40-sitelogs.conf local1.* /var/log/sitelogs/moodle/access.log local1.err /var/log/sitelogs/moodle/error.log local2.* /var/log/sitelogs/moodle/cron.log EOF service rsyslog restart # Fire off moodle setup if [ "$httpsTermination" = "None" ]; then siteProtocol="http" else siteProtocol="https" fi if [ $dbServerType = "mysql" ]; then if [ "$isMigration" = "true" ]; then echo "Importing database from the mysql dump file" if [ ! -f /moodle/migration-db-moodle.sql.tar.gz ]; then echo "Migrating moodle DB dump archive file not found." exit 1 fi tar -xvf /moodle/migration-db-moodle.sql.tar.gz -C /moodle/ if [ ! -f /moodle/migration-db-moodle.sql ]; then echo "Migrating moodle DB dump file not found." exit 1 fi echo "Importing migration moodle DB." mysql -h $mysqlIP -u $mysqladminlogin -p${mysqladminpass} ${moodledbname} < /moodle/migration-db-moodle.sql echo "Updating moodle db config settings" replace_moodle_config_value "dbhost" "$mysqlIP" replace_moodle_config_value "dbuser" "$azuremoodledbuser" replace_moodle_config_value "dbpass" "$moodledbpass" echo "Updating other moodle config settings" replace_moodle_config_value "dataroot" "\/moodle\/moodledata" replace_moodle_config_value "wwwroot" "$siteProtocol:\/\/$siteFQDN" else echo -e "cd /tmp; /usr/bin/php /moodle/html/moodle/admin/cli/install.php --chmod=770 --lang=en --wwwroot="$siteProtocol"://"$siteFQDN" --dataroot=/moodle/moodledata --dbhost="$mysqlIP" --dbname="$moodledbname" --dbuser="$azuremoodledbuser" --dbpass="$moodledbpass" --dbtype=mysqli --fullname='Moodle LMS' --shortname='Moodle' --adminuser=admin --adminpass="$adminpass" --adminemail=admin@"$siteFQDN" --non-interactive --agree-license --allow-unstable || true " cd /tmp; /usr/bin/php /moodle/html/moodle/admin/cli/install.php --chmod=770 --lang=en --wwwroot=$siteProtocol://$siteFQDN --dataroot=/moodle/moodledata --dbhost=$mysqlIP --dbname=$moodledbname --dbuser=$azuremoodledbuser --dbpass=$moodledbpass --dbtype=mysqli --fullname='Moodle LMS' --shortname='Moodle' --adminuser=admin --adminpass=$adminpass --adminemail=admin@$siteFQDN --non-interactive --agree-license --allow-unstable || true fi if [ "$installObjectFsSwitch" = "true" ]; then mysql -h $mysqlIP -u $mysqladminlogin -p${mysqladminpass} ${moodledbname} -e "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'enabletasks', 1);" mysql -h $mysqlIP -u $mysqladminlogin -p${mysqladminpass} ${moodledbname} -e "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'filesystem', '\\\tool_objectfs\\\azure_file_system');" mysql -h $mysqlIP -u $mysqladminlogin -p${mysqladminpass} ${moodledbname} -e "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'azure_accountname', '${storageAccountName}');" mysql -h $mysqlIP -u $mysqladminlogin -p${mysqladminpass} ${moodledbname} -e "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'azure_container', 'objectfs');" mysql -h $mysqlIP -u $mysqladminlogin -p${mysqladminpass} ${moodledbname} -e "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'azure_sastoken', '${sas}');" fi elif [ $dbServerType = "mssql" ]; then cd /tmp; /usr/bin/php /moodle/html/moodle/admin/cli/install.php --chmod=770 --lang=en --wwwroot=$siteProtocol://$siteFQDN --dataroot=/moodle/moodledata --dbhost=$mssqlIP --dbname=$moodledbname --dbuser=$azuremoodledbuser --dbpass=$moodledbpass --dbtype=sqlsrv --fullname='Moodle LMS' --shortname='Moodle' --adminuser=admin --adminpass=$adminpass --adminemail=admin@$siteFQDN --non-interactive --agree-license --allow-unstable || true if [ "$installObjectFsSwitch" = "true" ]; then /opt/mssql-tools/bin/sqlcmd -S $mssqlIP -U $mssqladminlogin -P ${mssqladminpass} -d ${moodledbname} -Q "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'enabletasks', 1)" /opt/mssql-tools/bin/sqlcmd -S $mssqlIP -U $mssqladminlogin -P ${mssqladminpass} -d ${moodledbname} -Q "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'filesystem', '\\\tool_objectfs\\\azure_file_system')" /opt/mssql-tools/bin/sqlcmd -S $mssqlIP -U $mssqladminlogin -P ${mssqladminpass} -d ${moodledbname} -Q "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'azure_accountname', '${storageAccountName}')" /opt/mssql-tools/bin/sqlcmd -S $mssqlIP -U $mssqladminlogin -P ${mssqladminpass} -d ${moodledbname} -Q "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'azure_container', 'objectfs')" /opt/mssql-tools/bin/sqlcmd -S $mssqlIP -U $mssqladminlogin -P ${mssqladminpass} -d${moodledbname} -Q "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'azure_sastoken', '${sas}')" fi else echo -e "cd /tmp; /usr/bin/php /moodle/html/moodle/admin/cli/install.php --chmod=770 --lang=en --wwwroot="$siteProtocol"://"$siteFQDN" --dataroot=/moodle/moodledata --dbhost="$postgresIP" --dbname="$moodledbname" --dbuser="$azuremoodledbuser" --dbpass="$moodledbpass" --dbtype=pgsql --fullname='Moodle LMS' --shortname='Moodle' --adminuser=admin --adminpass="$adminpass" --adminemail=admin@"$siteFQDN" --non-interactive --agree-license --allow-unstable || true " cd /tmp; /usr/bin/php /moodle/html/moodle/admin/cli/install.php --chmod=770 --lang=en --wwwroot=$siteProtocol://$siteFQDN --dataroot=/moodle/moodledata --dbhost=$postgresIP --dbname=$moodledbname --dbuser=$azuremoodledbuser --dbpass=$moodledbpass --dbtype=pgsql --fullname='Moodle LMS' --shortname='Moodle' --adminuser=admin --adminpass=$adminpass --adminemail=admin@$siteFQDN --non-interactive --agree-license --allow-unstable || true if [ "$installObjectFsSwitch" = "true" ]; then # Add the ObjectFS configuration to Moodle. echo "${postgresIP}:5432:${moodledbname}:${azuremoodledbuser}:${moodledbpass}" > /root/.pgpass chmod 600 /root/.pgpass psql -h $postgresIP -U $azuremoodledbuser -c "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'enabletasks', 1);" $moodledbname psql -h $postgresIP -U $azuremoodledbuser -c "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'filesystem', '\tool_objectfs\azure_file_system');" $moodledbname psql -h $postgresIP -U $azuremoodledbuser -c "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'azure_accountname', '$storageAccountName');" $moodledbname psql -h $postgresIP -U $azuremoodledbuser -c "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'azure_container', 'objectfs');" $moodledbname psql -h $postgresIP -U $azuremoodledbuser -c "INSERT INTO mdl_config_plugins (plugin, name, value) VALUES ('tool_objectfs', 'azure_sastoken', '$sas');" $moodledbname fi fi echo -e "\n\rDone! Installation completed!\n\r" # use /tmp/localcachedir/ for localcache and /var/www/html/moodle/ for core_component.php dir="/var/www/html/moodle" if [[ ! -d $dir ]]; then mkdir -p $dir fi sed -i "22 a \$CFG->localcachedir = '/tmp/localcachedir';" /moodle/html/moodle/config.php sed -i "22 a \$CFG->alternative_component_cache = '/var/www/html/moodle/core_component.php';" /moodle/html/moodle/config.php chown -R www-data:www-data $dir chgrp www-data $dir chmod g+s $dir if [ "$redisAuth" != "None" ]; then create_redis_configuration_in_moodledata_muc_config_php # redis configuration in /moodle/html/moodle/config.php sed -i "23 a \$CFG->session_redis_lock_expire = 7200;" /moodle/html/moodle/config.php sed -i "23 a \$CFG->session_redis_acquire_lock_timeout = 120;" /moodle/html/moodle/config.php sed -i "23 a \$CFG->session_redis_prefix = 'moodle_prod'; // Optional, default is don't set one." /moodle/html/moodle/config.php sed -i "23 a \$CFG->session_redis_database = 0; // Optional, default is db 0." /moodle/html/moodle/config.php sed -i "23 a \$CFG->session_redis_port = 6379; // Optional." /moodle/html/moodle/config.php sed -i "23 a \$CFG->session_redis_host = '$redisDns';" /moodle/html/moodle/config.php sed -i "23 a \$CFG->session_redis_auth = '$redisAuth';" /moodle/html/moodle/config.php sed -i "23 a \$CFG->session_handler_class = '\\\core\\\session\\\redis';" /moodle/html/moodle/config.php fi if [ "$httpsTermination" != "None" ]; then # We proxy ssl, so moodle needs to know this sed -i "23 a \$CFG->sslproxy = 'true';" /moodle/html/moodle/config.php fi if [ "$searchType" = "elastic" ]; then # Set up elasticsearch plugin if [ "$tikaVmIP" = "none" ]; then sed -i "23 a \$CFG->forced_plugin_settings = ['search_elastic' => ['hostname' => 'http://$elasticVm1IP']];" /moodle/html/moodle/config.php else sed -i "23 a \$CFG->forced_plugin_settings = ['search_elastic' => ['hostname' => 'http://$elasticVm1IP', 'fileindexing' => 'true', 'tikahostname' => 'http://$tikaVmIP', 'tikaport' => '9998'],];" /moodle/html/moodle/config.php fi sed -i "23 a \$CFG->searchengine = 'elastic';" /moodle/html/moodle/config.php sed -i "23 a \$CFG->enableglobalsearch = 'true';" /moodle/html/moodle/config.php # create index php /moodle/html/moodle/search/cli/indexer.php --force --reindex || true elif [ "$searchType" = "azure" ]; then # Set up Azure Search service plugin if [ "$tikaVmIP" = "none" ]; then sed -i "23 a \$CFG->forced_plugin_settings = ['search_azure' => ['searchurl' => 'https://$azureSearchNameHost', 'apikey' => '$azureSearchKey']];" /moodle/html/moodle/config.php else sed -i "23 a \$CFG->forced_plugin_settings = ['search_azure' => ['searchurl' => 'https://$azureSearchNameHost', 'apikey' => '$azureSearchKey', 'fileindexing' => '1', 'tikahostname' => 'http://$tikaVmIP', 'tikaport' => '9998'],];" /moodle/html/moodle/config.php fi sed -i "23 a \$CFG->searchengine = 'azure';" /moodle/html/moodle/config.php sed -i "23 a \$CFG->enableglobalsearch = 'true';" /moodle/html/moodle/config.php # create index php /moodle/html/moodle/search/cli/indexer.php --force --reindex || true fi if [ "$installObjectFsSwitch" = "true" ]; then # Set the ObjectFS alternate filesystem sed -i "23 a \$CFG->alternative_file_system_class = '\\\tool_objectfs\\\azure_file_system';" /moodle/html/moodle/config.php fi if [ "$dbServerType" = "postgres" ]; then # Get a new version of Postgres to match Azure version add-apt-repository "deb http://apt.postgresql.org/pub/repos/apt/ xenial-pgdg main" wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE='a' apt -key add - apt-get update apt-get install -y postgresql-client-9.6 fi # create cron entry # It is scheduled for once per minute. It can be changed as needed. echo '* * * * * www-data /usr/bin/php /moodle/html/moodle/admin/cli/cron.php 2>&1 | /usr/bin/logger -p local2.notice -t moodle' > /etc/cron.d/moodle-cron # Set up cronned sql dump if [ "$dbServerType" = "mysql" ]; then cat < /etc/cron.d/sql-backup 22 02 * * * root /usr/bin/mysqldump -h $mysqlIP -u ${azuremoodledbuser} -p'${moodledbpass}' --databases ${moodledbname} | gzip > /moodle/db-backup.sql.gz EOF elif [ "$dbServerType" = "postgres" ]; then cat < /etc/cron.d/sql-backup 22 02 * * * root /usr/bin/pg_dump -Fc -h $postgresIP -U ${azuremoodledbuser} ${moodledbname} > /moodle/db-backup.sql EOF #else # mssql. TODO It's missed earlier! Complete this! fi # Turning off services we don't need the controller running service nginx stop service php${PhpVer}-fpm stop service varnish stop service varnishncsa stop #service varnishlog stop # No need to run the commands below any more, as permissions & modes are already as such (no more "sudo -u www-data ...") # Leaving this code as a remark that we are explicitly leaving the ownership to root:root # if [ $fileServerType = "gluster" -o $fileServerType = "nfs" -o $fileServerType = "nfs-ha" ]; then # # make sure Moodle can read its code directory but not write # sudo chown -R root.root /moodle/html/moodle # sudo find /moodle/html/moodle -type f -exec chmod 644 '{}' \; # sudo find /moodle/html/moodle -type d -exec chmod 755 '{}' \; # fi # But now we need to adjust the moodledata and the certs directory ownerships, and the permission for the generated config.php sudo chown -R www-data.www-data /moodle/moodledata /moodle/certs sudo chmod +r /moodle/html/moodle/config.php # chmod /moodle for Azure NetApp Files (its default is 770!) if [ $fileServerType = "nfs-byo" ]; then sudo chmod +rx /moodle fi if [ $fileServerType = "azurefiles" ]; then if [ "$isMigration" = "true" ]; then echo -e '\n\rIts a migration flow, the moodle content is already on azure file share\n\r' else # Delayed copy of moodle installation to the Azure Files share # First rename moodle directory to something else mv /moodle /moodle_old_delete_me # Then create the moodle share echo -e '\n\rCreating an Azure Files share for moodle' create_azure_files_moodle_share $storageAccountName $storageAccountKey /tmp/wabs.log $fileServerDiskSize # Set up and mount Azure Files share. Must be done after nginx is installed because of www-data user/group echo -e '\n\rSetting up and mounting Azure Files share on //'$storageAccountName'.file.core.windows.net/moodle on /moodle\n\r' setup_and_mount_azure_files_moodle_share $storageAccountName $storageAccountKey # Move the local installation over to the Azure Files echo -e '\n\rMoving locally installed moodle over to Azure Files' # install azcopy wget -q -O azcopy_v10.tar.gz https://aka.ms/downloadazcopy-v10-linux && tar -xf azcopy_v10.tar.gz --strip-components=1 && mv ./azcopy /usr/bin/ ACCOUNT_KEY="$storageAccountKey" NAME="$storageAccountName" END=`date -u -d "60 minutes" '+%Y-%m-%dT%H:%M:00Z'` sas=$(az storage share generate-sas \ -n moodle \ --account-key $ACCOUNT_KEY \ --account-name $NAME \ --https-only \ --permissions lrw \ --expiry $END -o tsv) export AZCOPY_CONCURRENCY_VALUE='48' export AZCOPY_BUFFER_GB='4' # cp -a /moodle_old_delete_me/* /moodle || true # Ignore case sensitive directory copy failure azcopy --log-level ERROR copy "/moodle_old_delete_me/*" "https://$NAME.file.core.windows.net/moodle?$sas" --recursive || true # Ignore case sensitive directory copy failure rm -rf /moodle_old_delete_me || true # Keep the files just in case fi fi create_last_modified_time_update_script run_once_last_modified_time_update_script echo "### Script End `date`###" } 2>&1 | tee /tmp/install.log ================================================ FILE: scripts/install_tika.sh ================================================ #!/bin/bash # Custom Script for Linux # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. tikavmip=$1 echo $tikavmip >> /tmp/vars.txt { # make sure the system does automatic update sudo apt-get -y update sudo apt-get -y install unattended-upgrades # download apache tika server sudo wget -q http://mirrors.ocf.berkeley.edu/apache/tika/tika-server-1.18.jar --directory-prefix=/usr/share/java/ # install the required packages sudo apt-get install -y openjdk-8-jre openjdk-8-jdk default-jre default-jdk # Configure tika cat < /etc/systemd/system/tika-server.service [Unit] Description = Java Service After network.target = tika-server.service [Service] Type = forking ExecStart = /usr/local/bin/tika-server start ExecStop = /usr/local/bin/tika-server stop ExecReload = /usr/local/bin/tika-server reload [Install] WantedBy=multi-user.target EOF chmod 550 /etc/systemd/system/tika-server.service cat < /usr/local/bin/tika-server #!/bin/sh SERVICE_NAME=tika-server PATH_TO_JAR=/usr/share/java/tika-server-1.18.jar PID_PATH_NAME=/var/run/tika-server-pid case \$1 in start) echo "Starting \$SERVICE_NAME ..." if [ ! -f \$PID_PATH_NAME ]; then nohup java -jar \$PATH_TO_JAR --host=$tikavmip --port=9998 >> /var/log/tika-server.out 2>&1& echo \$! > \$PID_PATH_NAME echo "\$SERVICE_NAME started ..." else echo "\$SERVICE_NAME is already running ..." fi ;; stop) if [ -f \$PID_PATH_NAME ]; then PID=\$(cat $PID_PATH_NAME); echo "\$SERVICE_NAME stoping ..." kill \$PID; echo "\$SERVICE_NAME stopped ..." rm \$PID_PATH_NAME else echo "\$SERVICE_NAME is not running ..." fi ;; restart) if [ -f \$PID_PATH_NAME ]; then PID=\$(cat $PID_PATH_NAME); echo "\$SERVICE_NAME stopping ..."; kill \$PID; echo "\$SERVICE_NAME stopped ..."; rm \$PID_PATH_NAME echo "\$SERVICE_NAME starting ..." nohup java -jar \$PATH_TO_JAR --host=$tikavmip --port=9998 >> /var/log/tika-server.out 2>&1& echo \$! > \$PID_PATH_NAME echo "\$SERVICE_NAME started ..." else echo "\$SERVICE_NAME is not running ..." fi ;; esac EOF chmod +x /usr/local/bin/tika-server systemctl enable tika-server.service systemctl start tika-server.service } > /tmp/setup.log ================================================ FILE: scripts/setup_nfs_ha.sh ================================================ #!/bin/bash # # Script to set up highly available NFS server on an Ubuntu 16.04 (or higher) VM # that should be used on Azure with the custom script extension (which runs this script as root) # set -e # Parameters NODE1NAME=$1 NODE1IP=$2 NODE2NAME=$3 NODE2IP=$4 NFS_CLIENTS_IP_RANGE=$5 # E.g., "10.0.0.0/24". Can be "*", but strongly discouraged # This VM's IP address, to detect if this VM should be the master (Node 1 is the initial master) MY_IP=$(hostname -i) . ./helper_functions.sh function setup_required_packages { apt_update_noninteractive apt_install_noninteractive build-essential autoconf flex nfs-kernel-server corosync pacemaker resource-agents # Shouldn't let systemd start nfs-kernel-server (Pacemaker should do that) systemctl stop nfs-kernel-server systemctl disable nfs-kernel-server # Setup static port assignments for mountd, statd, quotad, nlm (tcp), and nlm (udp) respectively: sed -i 's/^\(RPCMOUNTDOPTS="--manage-gids\)"/\1 -p 2000"/g' /etc/default/nfs-kernel-server sed -i 's/^STATDOPTS=.*$/STATDOPTS="--port 2001 --outgoing-port 2002"/' /etc/default/nfs-common if [ -f /etc/default/quota ]; then sed -i 's/^RPCQUOTADOPTS=.*$/RPCQUOTADOPTS="-p 2003"/' /etc/default/quota fi cat < /etc/modprobe.d/azmdl-nfs-ports.conf options lockd nlm_udpport=2004 nlm_tcpport=2004 options nfs callback_tcpport=2005 EOF cat < /etc/sysctl.d/30-azmdl-nfs-ports.conf fs.nfs.nlm_tcpport=2004 fs.nfs.nlm_udpport=2004 EOF # Reread modified sysctl settings for modified NFS static ports sysctl --system # Above alone still doesn't work for static ports. Try restarting related services. systemctl try-restart nfs-config.service rpcbind.service rpc-statd.service nfs-server.service # We need to install the "azure-lb" command separately if the resource-agents package didn't have it. pushd /usr/lib/ocf/resource.d/heartbeat if [ ! -e azure-lb ]; then curl -LO https://raw.githubusercontent.com/ClusterLabs/resource-agents/master/heartbeat/azure-lb chmod +x ./azure-lb fi popd } function setup_drbd_module_and_tools { # We currently have to build the DRBD kernel module, as it's not included # in the default linux-azure kernels or in any Azure extra packages. # We should use the packaged DRBD module once Azure starts releasing # extra modules packages that include DRBD module. pushd /tmp git clone http://github.com/LINBIT/drbd-9.0 git clone http://github.com/LINBIT/drbd-utils cd drbd-9.0 make && make install modprobe drbd cd ../drbd-utils ./autogen.sh ./configure --prefix=/usr --localstatedir=/var --sysconfdir=/etc make tools && make install-tools cd .. rm -rf drbd-9.0 drbd-utils popd } function setup_drbd_with_disk { local disk=$1 local node1name=$2 local node1ip=$3 local node2name=$4 local node2ip=$5 local drbd_resource_name=$6 local drbd_device_path=$7 local drbd_device_mount_point=$8 # Put LVM (to gain more flexibility) on the whole disk local vgname=drbdvg local lvname=drbdlv wipefs -af $disk pvcreate $disk vgcreate $vgname $disk lvcreate -n $lvname -l 95%VG $vgname # Set up DRBD config cat < /etc/drbd.d/${drbd_resource_name}.res resource $drbd_resource_name { device ${drbd_device_path}; disk /dev/${vgname}/${lvname}; meta-disk internal; disk { c-fill-target 1M; c-max-rate 110M; c-min-rate 120K; } net { max-buffers 20k; } on ${node1name} { address ${node1ip}:7789; } on ${node2name} { address ${node2ip}:7789; } } EOF # Initialize DRBD's metadata and bring up the device on both nodes drbdadm create-md $drbd_resource_name drbdadm up $drbd_resource_name drbdadm status # On the master (initially node 1) only, force DRBD into Primary and create an ext4 file system on the DRBD device if [ "$MY_IP" = "$NODE1IP" ]; then drbdadm primary $drbd_resource_name --force mkfs.ext4 $drbd_device_path mkdir -p $drbd_device_mount_point && mount $drbd_device_path $drbd_device_mount_point fi } function setup_corosync_and_pacemaker_for_nfs { local node1ip=$1 local node2ip=$2 local drbd_resource_name=$3 # E.g., azmdlr0 local drbd_device_path=$4 # E.g., /dev/drbd0 local drbd_mount_point=$5 # E.g., /drbd local nfs_export_path=$6 # E.g., /drbd/moodle local nfs_client_spec=$7 # E.g., * or 10.11.22.0/24 mv /etc/corosync/corosync.conf /etc/corosync/corosync.conf.orig || true local cluster_name=azmdl-cluster cat < /etc/corosync/corosync.conf totem { version: 2 secauth: off cluster_name: ${cluster_name} transport: udpu } nodelist { node { ring0_addr: ${node1ip} nodeid: 1 } node { ring0_addr: ${node2ip} nodeid: 2 } } quorum { provider: corosync_votequorum two_node:1 } logging { to_syslog: yes } EOF systemctl enable corosync pacemaker systemctl restart corosync pacemaker # TODO Should confirm if 'corosync-cfgtool -s' gives a non-loopback IP address (not 127.0.0.1), e.g.: # $ corosync-cfgtool -s # Printing ring status. # Local node ID 2 # RING ID 0 # id = 10.0.0.5 # status = ring 0 active with no faults # Finally, configure Pacemaker cluster resources, only on the initial master if [ "$MY_IP" = "$node1ip" ]; then mkdir -p ${nfs_export_path} crm configure <> /tmp/vars.txt echo "$glusterVolume" >> /tmp/vars.txt echo "$siteFQDN" >> /tmp/vars.txt echo "$httpsTermination" >> /tmp/vars.txt echo "$syslogServer" >> /tmp/vars.txt echo "$webServerType" >> /tmp/vars.txt echo "$dbServerType" >> /tmp/vars.txt echo "$fileServerType" >> /tmp/vars.txt echo "$storageAccountName" >> /tmp/vars.txt echo "$storageAccountKey" >> /tmp/vars.txt echo "$nfsVmName" >> /tmp/vars.txt echo "$nfsByoIpExportPath" >> /tmp/vars.txt echo "$htmlLocalCopySwitch" >> /tmp/vars.txt echo "$phpVersion" >> /tmp/vars.txt check_fileServerType_param "$fileServerType" { set -ex echo "### Function Start $(date)###" # add azure-cli repository && install azure cli AZ_REPO=$(lsb_release -cs) mkdir -p /etc/apt/keyrings curl -sLS https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/keyrings/microsoft.gpg && chmod go+r /etc/apt/keyrings/microsoft.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/microsoft.gpg] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" > /etc/apt/sources.list.d/azure-cli.list apt_update_noninteractive # install pre-requisites including VARNISH and PHP-FPM apt_install_noninteractive \ azure-cli \ ca-certificates \ curl \ apt-transport-https \ lsb-release gnupg \ software-properties-common \ unzip \ rsyslog \ postgresql-client \ mysql-client \ git \ unattended-upgrades \ tuned \ varnish \ php \ php-cli \ php-curl \ php-zip \ php-pear \ php-mbstring \ mcrypt \ php-dev \ graphviz \ aspell \ php-soap \ php-json \ php-redis \ php-bcmath \ php-ldap \ php-gd \ php-pgsql \ php-mysql \ php-xmlrpc \ php-intl \ php-xml \ php-bz2 # install azcopy wget -q -O azcopy_v10.tar.gz https://aka.ms/downloadazcopy-v10-linux && tar -xf azcopy_v10.tar.gz --strip-components=1 && mv ./azcopy /usr/bin/ # kernel settings cat < /etc/sysctl.d/99-network-performance.conf net.core.somaxconn = 65536 net.core.netdev_max_backlog = 5000 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.ipv4.tcp_wmem = 4096 12582912 16777216 net.ipv4.tcp_rmem = 4096 12582912 16777216 net.ipv4.route.flush = 1 net.ipv4.tcp_max_syn_backlog = 8096 net.ipv4.tcp_tw_reuse = 1 net.ipv4.ip_local_port_range = 10240 65535 EOF # apply the new kernel settings sysctl -p /etc/sysctl.d/99-network-performance.conf # scheduling IRQ interrupts on the last two cores of the cpu # masking 0011 or 00000011 the result will always be 3 echo "obase=16;ibase=2;0011" | bc | tr '[:upper:]' '[:lower:]' if [ -f /etc/default/irqbalance ]; then sed -i "s/\#IRQBALANCE_BANNED_CPUS\=/IRQBALANCE_BANNED_CPUS\=3/g" /etc/default/irqbalance systemctl restart irqbalance.service fi # configuring tuned for throughput-performance systemctl enable tuned tuned-adm profile throughput-performance if [ "$fileServerType" = "gluster" ]; then #configure gluster repository & install gluster client add-apt-repository ppa:gluster/glusterfs-9 --yes apt_update_noninteractive apt_install_noninteractive glusterfs-client elif [ "$fileServerType" = "azurefiles" ]; then apt_install_noninteractive \ linux-modules-extra-azure \ cifs-utils >> /tmp/apt.log # Because of https://bugs.launchpad.net/ubuntu/+source/linux-azure/+bug/2042092 modprobe cifs && true fi if [ "$webServerType" = "nginx" -o "$httpsTermination" = "VMSS" ]; then apt_install_noninteractive nginx fi if [ "$webServerType" = "apache" ]; then # install apache pacakges apt_install_noninteractive apache2 libapache2-mod-php else # for nginx-only option apt_install_noninteractive php-fpm fi # Moodle requirements if [ "$dbServerType" = "mssql" ]; then install_php_mssql_driver fi # PHP Version PhpVer=$(get_php_version) if [ "$fileServerType" = "gluster" ]; then # Mount gluster fs for /moodle sudo mkdir -p /moodle sudo chown www-data /moodle sudo chmod 770 /moodle sudo echo -e 'Adding Gluster FS to /etc/fstab and mounting it' setup_and_mount_gluster_moodle_share "$glusterNode" "$glusterVolume" elif [ $fileServerType = "nfs" ]; then # mount NFS export (set up on controller VM--No HA) echo -e '\n\rMounting NFS export from '$nfsVmName':/moodle on /moodle and adding it to /etc/fstab\n\r' configure_nfs_client_and_mount $nfsVmName /moodle /moodle elif [ $fileServerType = "nfs-ha" ]; then # mount NFS-HA export echo -e '\n\rMounting NFS export from '$nfsHaLbIP':'$nfsHaExportPath' on /moodle and adding it to /etc/fstab\n\r' configure_nfs_client_and_mount $nfsHaLbIP $nfsHaExportPath /moodle elif [ $fileServerType = "nfs-byo" ]; then # mount NFS-BYO export echo -e '\n\rMounting NFS export from '$nfsByoIpExportPath' on /moodle and adding it to /etc/fstab\n\r' configure_nfs_client_and_mount0 $nfsByoIpExportPath /moodle else # "azurefiles" setup_and_mount_azure_files_moodle_share $storageAccountName $storageAccountKey fi # Configure syslog to forward cat <> /etc/rsyslog.conf \$ModLoad imudp \$UDPServerRun 514 EOF cat <> /etc/rsyslog.d/40-remote.conf local1.* @${syslogServer}:514 local2.* @${syslogServer}:514 EOF service syslog restart if [ "$webServerType" = "nginx" -o "$httpsTermination" = "VMSS" ]; then # Build nginx config cat < /etc/nginx/nginx.conf user www-data; worker_processes auto; pid /run/nginx.pid; events { worker_connections 8192; multi_accept on; use epoll; } worker_rlimit_nofile 100000; http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; client_max_body_size 0; proxy_max_temp_file_size 0; server_names_hash_bucket_size 128; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; proxy_buffering off; include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; open_file_cache max=20000 inactive=20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on; set_real_ip_from 127.0.0.1; real_ip_header X-Forwarded-For; #upgrading to TLSv1.2 and droping 1 & 1.1 ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; #adding ssl ciphers ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; gzip on; gzip_disable "msie6"; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; EOF if [ "$httpsTermination" != "None" ]; then cat <> /etc/nginx/nginx.conf map \$http_x_forwarded_proto \$fastcgi_https { default \$https; http ''; https on; } EOF fi cat <> /etc/nginx/nginx.conf log_format moodle_combined '\$remote_addr - \$upstream_http_x_moodleuser [\$time_local] ' '"\$request" \$status \$body_bytes_sent ' '"\$http_referer" "\$http_user_agent"'; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; } EOF fi # if [ "$webServerType" = "nginx" -o "$httpsTermination" = "VMSS" ]; # Set up html dir local copy if specified htmlRootDir="/moodle/html/moodle" if [ "$htmlLocalCopySwitch" = "true" ]; then if [ "$fileServerType" = "azurefiles" ]; then mkdir -p /var/www/html ACCOUNT_KEY="$storageAccountKey" NAME="$storageAccountName" END=$(date -u -d "60 minutes" '+%Y-%m-%dT%H:%M:00Z') htmlRootDir="/var/www/html/moodle" sas=$(az storage share generate-sas \ -n moodle \ --account-key $ACCOUNT_KEY \ --account-name $NAME \ --https-only \ --permissions lr \ --expiry $END -o tsv) export AZCOPY_CONCURRENCY_VALUE='48' export AZCOPY_BUFFER_GB='4' azcopy --log-level ERROR copy "https://$NAME.file.core.windows.net/moodle/html/moodle/*?$sas" $htmlRootDir --recursive chown www-data:www-data -R $htmlRootDir && sync setup_html_local_copy_cron_job fi if [ "$fileServerType" = "nfs" -o "$fileServerType" = "nfs-ha" -o "$fileServerType" = "nfs-byo" -o "$fileServerType" = "gluster" ]; then mkdir -p /var/www/html/moodle rsync -a /moodle/html/moodle/ $htmlRootDir/ chown www-data:www-data -R $htmlRootDir && sync setup_html_local_copy_cron_job fi fi if [ "$httpsTermination" = "VMSS" ]; then # Configure nginx/https cat <> /etc/nginx/sites-enabled/${siteFQDN}.conf server { listen 443 ssl http2; root ${htmlRootDir}; index index.php index.html index.htm; ssl_certificate /moodle/certs/nginx.crt; ssl_certificate_key /moodle/certs/nginx.key; ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; # Log to syslog error_log syslog:server=localhost,facility=local1,severity=error,tag=moodle; access_log syslog:server=localhost,facility=local1,severity=notice,tag=moodle moodle_combined; # Log XFF IP instead of varnish set_real_ip_from 10.0.0.0/8; set_real_ip_from 127.0.0.1; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; real_ip_header X-Forwarded-For; real_ip_recursive on; location / { proxy_set_header Host \$host; proxy_set_header HTTP_REFERER \$http_referer; proxy_set_header X-Forwarded-Host \$host; proxy_set_header X-Forwarded-Server \$host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_pass http://localhost:80; proxy_next_upstream error timeout http_502 http_504; proxy_connect_timeout 3600; proxy_send_timeout 3600; proxy_read_timeout 3600; send_timeout 3600; } } EOF fi if [ "$webServerType" = "nginx" ]; then cat <> /etc/nginx/sites-enabled/${siteFQDN}.conf server { listen 81 default; server_name ${siteFQDN}; root ${htmlRootDir}; index index.php index.html index.htm; # Log to syslog error_log syslog:server=localhost,facility=local1,severity=error,tag=moodle; access_log syslog:server=localhost,facility=local1,severity=notice,tag=moodle moodle_combined; # Log XFF IP instead of varnish set_real_ip_from 10.0.0.0/8; set_real_ip_from 127.0.0.1; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; real_ip_header X-Forwarded-For; real_ip_recursive on; EOF if [ "$httpsTermination" != "None" ]; then cat <> /etc/nginx/sites-enabled/${siteFQDN}.conf # Redirect to https if (\$http_x_forwarded_proto != https) { return 301 https://\$server_name\$request_uri; } rewrite ^/(.*\.php)(/)(.*)$ /\$1?file=/\$3 last; EOF fi cat <> /etc/nginx/sites-enabled/${siteFQDN}.conf # Filter out php-fpm status page location ~ ^/server-status { return 404; } location / { try_files \$uri \$uri/index.php?\$query_string; } location ~ [^/]\.php(/|$) { fastcgi_split_path_info ^(.+?\.php)(/.*)$; if (!-f \$document_root\$fastcgi_script_name) { return 404; } fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; fastcgi_pass backend; fastcgi_param PATH_INFO \$fastcgi_path_info; fastcgi_read_timeout 3600; fastcgi_index index.php; include fastcgi_params; } } upstream backend { server unix:/run/php/php${PhpVer}-fpm.sock fail_timeout=1s; server unix:/run/php/php${PhpVer}-fpm-backup.sock backup; } EOF fi if [ "$webServerType" = "apache" ]; then # Configure Apache/php sed -i "s/Listen 80/Listen 81/" /etc/apache2/ports.conf a2enmod rewrite && a2enmod remoteip && a2enmod headers cat <> /etc/apache2/sites-enabled/${siteFQDN}.conf ServerName ${siteFQDN} ServerAdmin webmaster@localhost DocumentRoot ${htmlRootDir} Options FollowSymLinks AllowOverride All Require all granted EOF if [ "$httpsTermination" != "None" ]; then cat <> /etc/apache2/sites-enabled/${siteFQDN}.conf # Redirect unencrypted direct connections to HTTPS RewriteEngine on RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC] RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,R=301] EOF fi cat <> /etc/apache2/sites-enabled/${siteFQDN}.conf # Log X-Forwarded-For IP address instead of varnish (127.0.0.1) SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" forwarded ErrorLog "|/usr/bin/logger -t moodle -p local1.error" CustomLog "|/usr/bin/logger -t moodle -p local1.notice" combined env=!forwarded CustomLog "|/usr/bin/logger -t moodle -p local1.notice" forwarded env=forwarded EOF fi # if [ "$webServerType" = "apache" ]; # php config if [ "$webServerType" = "apache" ]; then PhpIni=/etc/php/${PhpVer}/apache2/php.ini else PhpIni=/etc/php/${PhpVer}/fpm/php.ini fi sed -i "s/memory_limit.*/memory_limit = 512M/" $PhpIni sed -i "s/max_execution_time.*/max_execution_time = 18000/" $PhpIni sed -i "s/;max_input_vars.*/max_input_vars = 100000/" $PhpIni sed -i "s/max_input_time.*/max_input_time = 600/" $PhpIni sed -i "s/upload_max_filesize.*/upload_max_filesize = 1024M/" $PhpIni sed -i "s/post_max_size.*/post_max_size = 1056M/" $PhpIni sed -i "s/;opcache.use_cwd.*/opcache.use_cwd = 1/" $PhpIni sed -i "s/;opcache.validate_timestamps.*/opcache.validate_timestamps = 1/" $PhpIni sed -i "s/;opcache.save_comments.*/opcache.save_comments = 1/" $PhpIni sed -i "s/;opcache.enable_file_override.*/opcache.enable_file_override = 0/" $PhpIni sed -i "s/;opcache.enable.*/opcache.enable = 1/" $PhpIni sed -i "s/;opcache.memory_consumption.*/opcache.memory_consumption = 512/" $PhpIni sed -i "s/;opcache.max_accelerated_files.*/opcache.max_accelerated_files = 20000/" $PhpIni # Remove the default site. Moodle is the only site we want rm -f /etc/nginx/sites-enabled/default if [ "$webServerType" = "apache" ]; then rm -f /etc/apache2/sites-enabled/000-default.conf fi if [ "$webServerType" = "nginx" -o "$httpsTermination" = "VMSS" ]; then # update startup script to wait for certificate in /moodle mount setup_moodle_mount_dependency_for_systemd_service nginx || exit 1 # restart Nginx sudo service nginx restart fi # Configure varnish startup for 18.04 VARNISHSTART="ExecStart=\/usr\/sbin\/varnishd -j unix,user=vcache -F -a :80 -T localhost:6082 -f \/etc\/varnish\/moodle.vcl -S \/etc\/varnish\/secret -s malloc,4096m -p thread_pool_min=1000 -p thread_pool_max=4000 -p thread_pool_add_delay=0.1 -p timeout_linger=10 -p timeout_idle=30 -p send_timeout=1800 -p thread_pools=2 -p http_max_hdr=512 -p workspace_backend=512k" sed -i "s/^ExecStart.*/${VARNISHSTART}/" /lib/systemd/system/varnish.service # Configure varnish VCL for moodle cat <> /etc/varnish/moodle.vcl vcl 4.0; import std; import directors; backend default { .host = "localhost"; .port = "81"; .first_byte_timeout = 3600s; .connect_timeout = 600s; .between_bytes_timeout = 600s; } sub vcl_recv { # Varnish does not support SPDY or HTTP/2.0 untill we upgrade to Varnish 5.0 if (req.method == "PRI") { return (synth(405)); } if (req.restarts == 0) { if (req.http.X-Forwarded-For) { set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip; } else { set req.http.X-Forwarded-For = client.ip; } } # Non-RFC2616 or CONNECT HTTP requests methods filtered. Pipe requests directly to backend if (req.method != "GET" && req.method != "HEAD" && req.method != "PUT" && req.method != "POST" && req.method != "TRACE" && req.method != "OPTIONS" && req.method != "DELETE") { return (pipe); } # Varnish don't mess with healthchecks if (req.url ~ "^/admin/tool/heartbeat" || req.url ~ "^/healthcheck.php") { return (pass); } # Pipe requests to backup.php straight to backend - prevents problem with progress bar long polling 503 problem # This is here because backup.php is POSTing to itself - Filter before !GET&&!HEAD if (req.url ~ "^/backup/backup.php") { return (pipe); } # Varnish only deals with GET and HEAD by default. If request method is not GET or HEAD, pass request to backend if (req.method != "GET" && req.method != "HEAD") { return (pass); } ### Rules for Moodle and Totara sites ### # Moodle doesn't require Cookie to serve following assets. Remove Cookie header from request, so it will be looked up. if ( req.url ~ "^/altlogin/.+/.+\.(png|jpg|jpeg|gif|css|js|webp)$" || req.url ~ "^/pix/.+\.(png|jpg|jpeg|gif)$" || req.url ~ "^/theme/font.php" || req.url ~ "^/theme/image.php" || req.url ~ "^/theme/javascript.php" || req.url ~ "^/theme/jquery.php" || req.url ~ "^/theme/styles.php" || req.url ~ "^/theme/yui" || req.url ~ "^/lib/javascript.php/-1/" || req.url ~ "^/lib/requirejs.php/-1/" ) { set req.http.X-Long-TTL = "86400"; unset req.http.Cookie; return(hash); } # Perform lookup for selected assets that we know are static but Moodle still needs a Cookie if( req.url ~ "^/theme/.+\.(png|jpg|jpeg|gif|css|js|webp)" || req.url ~ "^/lib/.+\.(png|jpg|jpeg|gif|css|js|webp)" || req.url ~ "^/pluginfile.php/[0-9]+/course/overviewfiles/.+\.(?i)(png|jpg)$" ) { # Set internal temporary header, based on which we will do things in vcl_backend_response set req.http.X-Long-TTL = "86400"; return (hash); } # Serve requests to SCORM checknet.txt from varnish. Have to remove get parameters. Response body always contains "1" if ( req.url ~ "^/lib/yui/build/moodle-core-checknet/assets/checknet.txt" ) { set req.url = regsub(req.url, "(.*)\?.*", "\1"); unset req.http.Cookie; # Will go to hash anyway at the end of vcl_recv set req.http.X-Long-TTL = "86400"; return(hash); } # Requests containing "Cookie" or "Authorization" headers will not be cached if (req.http.Authorization || req.http.Cookie) { return (pass); } # Almost everything in Moodle correctly serves Cache-Control headers, if # needed, which varnish will honor, but there are some which don't. Rather # than explicitly finding them all and listing them here we just fail safe # and don't cache unknown urls that get this far. return (pass); } sub vcl_backend_response { # Happens after we have read the response headers from the backend. # # Here you clean the response headers, removing silly Set-Cookie headers # and other mistakes your backend does. # We know these assest are static, let's set TTL >0 and allow client caching if ( beresp.http.Cache-Control && bereq.http.X-Long-TTL && beresp.ttl < std.duration(bereq.http.X-Long-TTL + "s", 1s) && !beresp.http.WWW-Authenticate ) { # If max-age < defined in X-Long-TTL header set beresp.http.X-Orig-Pragma = beresp.http.Pragma; unset beresp.http.Pragma; set beresp.http.X-Orig-Cache-Control = beresp.http.Cache-Control; set beresp.http.Cache-Control = "public, max-age="+bereq.http.X-Long-TTL+", no-transform"; set beresp.ttl = std.duration(bereq.http.X-Long-TTL + "s", 1s); unset bereq.http.X-Long-TTL; } else if( !beresp.http.Cache-Control && bereq.http.X-Long-TTL && !beresp.http.WWW-Authenticate ) { set beresp.http.X-Orig-Pragma = beresp.http.Pragma; unset beresp.http.Pragma; set beresp.http.Cache-Control = "public, max-age="+bereq.http.X-Long-TTL+", no-transform"; set beresp.ttl = std.duration(bereq.http.X-Long-TTL + "s", 1s); unset bereq.http.X-Long-TTL; } else { # Don't touch headers if max-age > defined in X-Long-TTL header unset bereq.http.X-Long-TTL; } # Here we set X-Trace header, prepending it to X-Trace header received from backend. Useful for troubleshooting if(beresp.http.x-trace && !beresp.was_304) { set beresp.http.X-Trace = regsub(server.identity, "^([^.]+),?.*$", "\1")+"->"+regsub(beresp.backend.name, "^(.+)\((?:[0-9]{1,3}\.){3}([0-9]{1,3})\)","\1(\2)")+"->"+beresp.http.X-Trace; } else { set beresp.http.X-Trace = regsub(server.identity, "^([^.]+),?.*$", "\1")+"->"+regsub(beresp.backend.name, "^(.+)\((?:[0-9]{1,3}\.){3}([0-9]{1,3})\)","\1(\2)"); } # Gzip JS, CSS is done at the ngnix level doing it here dosen't respect the no buffer requsets # if (beresp.http.content-type ~ "application/javascript.*" || beresp.http.content-type ~ "text") { # set beresp.do_gzip = true; #} } sub vcl_deliver { # Revert back to original Cache-Control header before delivery to client if (resp.http.X-Orig-Cache-Control) { set resp.http.Cache-Control = resp.http.X-Orig-Cache-Control; unset resp.http.X-Orig-Cache-Control; } # Revert back to original Pragma header before delivery to client if (resp.http.X-Orig-Pragma) { set resp.http.Pragma = resp.http.X-Orig-Pragma; unset resp.http.X-Orig-Pragma; } # (Optional) X-Cache HTTP header will be added to responce, indicating whether object was retrieved from backend, or served from cache if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; } else { set resp.http.X-Cache = "MISS"; } # Set X-AuthOK header when totara/varnsih authentication succeeded if (req.http.X-AuthOK) { set resp.http.X-AuthOK = req.http.X-AuthOK; } # If desired "Via: 1.1 Varnish-v4" response header can be removed from response unset resp.http.Via; unset resp.http.Server; return(deliver); } sub vcl_backend_error { # More comprehensive varnish error page. Display time, instance hostname, host header, url for easier troubleshooting. set beresp.http.Content-Type = "text/html; charset=utf-8"; set beresp.http.Retry-After = "5"; synthetic( {" "} + beresp.status + " " + beresp.reason + {"

Error "} + beresp.status + " " + beresp.reason + {"

"} + beresp.reason + {"

Guru Meditation:

Time: "} + now + {"

Node: "} + server.hostname + {"

Host: "} + bereq.http.host + {"

URL: "} + bereq.url + {"

XID: "} + bereq.xid + {"


Varnish cache server "} ); return (deliver); } sub vcl_synth { #Redirect using '301 - Permanent Redirect', permanent redirect if (resp.status == 851) { set resp.http.Location = req.http.x-redir; set resp.http.X-Varnish-Redirect = true; set resp.status = 301; return (deliver); } #Redirect using '302 - Found', temporary redirect if (resp.status == 852) { set resp.http.Location = req.http.x-redir; set resp.http.X-Varnish-Redirect = true; set resp.status = 302; return (deliver); } #Redirect using '307 - Temporary Redirect', !GET&&!HEAD requests, dont change method on redirected requests if (resp.status == 857) { set resp.http.Location = req.http.x-redir; set resp.http.X-Varnish-Redirect = true; set resp.status = 307; return (deliver); } #Respond with 403 - Forbidden if (resp.status == 863) { set resp.http.X-Varnish-Error = true; set resp.status = 403; return (deliver); } } EOF # This code is stop apache2 which is installing in 18.04 service=apache2 if [ "$webServerType" = "nginx" ]; then if pgrep -x "$service" >/dev/null then echo "Stop the $service!!!" systemctl stop $service else systemctl mask $service fi fi # Restart Varnish systemctl daemon-reload systemctl restart varnish if [ "$webServerType" = "nginx" ]; then # fpm config - overload this cat < /etc/php/${PhpVer}/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /run/php/php${PhpVer}-fpm.sock listen.owner = www-data listen.group = www-data pm = static pm.max_children = 32 pm.start_servers = 32 pm.max_requests = 300000 EOF cat < /etc/php/${PhpVer}/fpm/pool.d/backup.conf [backup] user = www-data group = www-data listen = /run/php/php${PhpVer}-fpm-backup.sock listen.owner = www-data listen.group = www-data pm = static pm.max_children = 16 pm.start_servers = 16 pm.max_requests = 300000 EOF # Restart fpm service php${PhpVer}-fpm restart fi if [ "$webServerType" = "apache" ]; then if [ "$htmlLocalCopySwitch" != "true" ]; then setup_moodle_mount_dependency_for_systemd_service apache2 || exit 1 fi service apache2 restart fi echo "### Script End $(date)###" } 2>&1 | tee /tmp/setup.log