├── CMakeLists.txt ├── LICENSE ├── NOTICE ├── README.md ├── dist └── bamboo-automate.spec └── lib ├── CMakeLists.txt ├── __init__.py ├── bamboo ├── CMakeLists.txt ├── __init__.py ├── agents.py ├── authenticate.py ├── branches.py ├── jobs.py ├── permissions.py ├── plans.py ├── requirements.py ├── results.py ├── tasks.py └── variables.py ├── bamboo_automate.py ├── high_level_functions.py ├── manipulate_bamboo_json.py ├── prettyprint_functions.py └── requests.py /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.6) 2 | 3 | project (bamboo-automate) 4 | 5 | # Python libraries location 6 | execute_process (COMMAND python -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)" 7 | OUTPUT_VARIABLE PYTHON_SITE_PACKAGES 8 | OUTPUT_STRIP_TRAILING_WHITESPACE) 9 | 10 | # Subdirectories 11 | add_subdirectory (lib) 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | bamboo-automate Copyright 2013 Martin Philipp Hellmich (mhellmic_AT_gmail) 2 | 3 | The lxml library (http://lxml.de) is used under the terms of the BSD license and is copyright Infrae. 4 | See git://github.com/lxml/lxml.git, LICENSES.txt for details. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Bamboo Automation 3 | 4 | This set of tools should allow to automate some of the task you want to do on Bamboo. 5 | It uses the rest api whenever possible and the web ui otherwise. 6 | 7 | ## Install 8 | 9 | * Checkout the source from git 10 | 11 | ## Structure 12 | 13 | lib/requests.py 14 | lib/bamboo_commands.py 15 | lib/manipulate_bamboo_json.py 16 | 17 | 18 | * **lib/requests.py** handles the http requests 19 | * **lib/bamboo_commands.py** provides bamboo actions like add_job_requirement() 20 | * **lib/manipulate_bamboo_json.py** retrieve values from the bamboo results 21 | * **main dir** holds example files. that will work on a local dev bamboo, but be notoriously out of date 22 | 23 | ## Examples 24 | This section lists the usable commands. 25 | 26 | They assume that you have imported the libraries like this 27 | 28 | from lib.bamboo_commands import * 29 | from lib.manipulate_bamboo_json import * 30 | 31 | ### Create Connection 32 | Either use username and password 33 | 34 | conn = authenticate('http://localhost:6990', 'admin', 'admin', '/bamboo') 35 | 36 | or take a JSESSIONID cookie from a file in the Mozilla format 37 | 38 | conn = external_authenticate('http://localhost:6990', 'cookies.txt', '/bamboo') 39 | 40 | (there is a firefox extension to export cookies) 41 | 42 | The last parameter specifies a directory prefix, where bamboo can be found. If you don't need a prefix, don't specify it or make it empty (''). 43 | 44 | ### List Projects 45 | Lists all projects 46 | 47 | project_dict = get_projects(conn) 48 | 49 | You can use the expand option with the valid parameters from [bamboo's REST API definition] (https://developer.atlassian.com/display/BAMBOODEV/Bamboo+REST+Resources#BambooRESTResources-ProjectService "Bamboo REST API Projects") 50 | 51 | project_dict = get_projects(conn, expand="projects.project.plans) 52 | 53 | ### List Plans 54 | List all plans 55 | 56 | plan_dict = get_plans(conn) 57 | 58 | You can use the expand option with the valid parameters from [bamboo's REST API definition] (https://developer.atlassian.com/display/BAMBOODEV/Bamboo+REST+Resources#BambooRESTResources-PlanService "Bamboo REST API Projects") 59 | 60 | project_dict = get_projects(conn, expand="plans.plan) 61 | 62 | ### Get Project and Plan Keys 63 | Get the keys from the bamboo json output. 64 | Plan keys include the project key in a projectkey-plankey schema, so you can access the plan with the plan key only. 65 | The keys are unique string representations for a project or plan. 66 | 67 | project_keys = get_project_keys(project_dict) 68 | plan_keys = get_plan_keys(plan_dict) 69 | 70 | ### Manipulate Variables 71 | Get the plan key first 72 | 73 | plan_key = 'PROJECTKEY-PLANKEY' 74 | 75 | #### Add a Variable 76 | This will fail if the variable already exists 77 | 78 | res = add_plan_variable(conn, plan_key, 'varkey', varvalue') 79 | 80 | #### Modify a Variable 81 | This will only work if the variable already exists 82 | 83 | res = mod_plan_variable(conn, plan_key, 'varkey', 'varvalue') 84 | 85 | #### Add/Modify a Variable 86 | Tries to add the variable and modifies if it exists 87 | 88 | res = add_mod_plan_variable(conn, plan_key, 'varkey', 'varvalue') 89 | 90 | #### Delete Variables 91 | You can delete one variable specified by key or delete all at once 92 | 93 | res = delete_plan_variable(conn, plan_key, 'varkey') 94 | res = delete_plan_all_variables(conn, plan_key) 95 | 96 | ### Get Job ID 97 | Get the ID of the plan's jobs. 98 | For jobs the key is the name presented on the website, the id the unique internal string representation. 99 | The result is different from get _ projects() or get _ plans(). It returns a dict of tuples. 100 | 101 | job_key = 'Build' 102 | job_dict = get_jobs(conn, plan_key) 103 | job_id = job_dict[job_key][0] 104 | 105 | ### Manipulate Requirements 106 | Requirements are job-bound, so we need the job ID 107 | 108 | job_id = 'PROJECTKEY-PLANKEY-JOB1' 109 | 110 | #### Add a Requirement 111 | This will fail if the requirement already exists. 112 | If you add a requirement that is already in the system, the parameter req _ exists must be True. 113 | 114 | res = add_job_requirement(conn, job_id, 'reqkey', 'reqvalue', req_exists=False) 115 | 126 | #### Delete Requirements 127 | You can delete one requirement specified by key or delete all at once 128 | 129 | res = delete_job_requirement(conn, job_id, 'reqkey') 130 | res = delete_job_all_requirements(conn, job_id) 131 | 132 | ### Manipulating Tasks 133 | Tasks are job-bound, so we need the job ID. 134 | We also need the task key and the task parameters. This example shows the sourcecode checkout task. 135 | Fields that are not needed, like userDescription, can be left empty (""). 136 | 137 | job_id = 'PROJECTKEY-PLANKEY-JOB1' 138 | 139 | 140 | task_key = "com.atlassian.bamboo.plugins.vcs:task.vcs.checkout" 141 | task_params = { 142 | "checkBoxFields": "cleanCheckout", 143 | "checkoutDir_0": "", 144 | "selectFields": "selectedRepository_0", 145 | "selectedRepository_0": "defaultRepository", 146 | "userDescription": "" 147 | } 148 | 149 | #### Add a Task 150 | A task can be added multiple times without problems 151 | 152 | res = add_job_task(conn, job_id, task_key, task_params) 153 | 154 | #### Move a Task 155 | In bamboo you can move a task precisely. This function, however, only puts tasks into the final stage or back so far. 156 | This function needs the task id, which can be best retrieved with the task description. 157 | 158 | task_description = "" 159 | task_dict = get_tasks(conn, job_id) 160 | for task_id, (task_key, description, edit_link, del_link) in job_tasks.iteritems(): 161 | if description == task_description: 162 | res = move_job_task(conn, job_id, task_id, finalising=True) 163 | 164 | #### Delete Task 165 | This function needs the task id, which can be best retrieved with the task description. 166 | 167 | task_description = "" 168 | task_dict = get_tasks(conn, job_id) 169 | for task_id, (task_key, description, edit_link, del_link) in job_tasks.iteritems(): 170 | if description == task_description: 171 | res = delete_job_task(conn, job_id, task_id) 172 | 173 | ### Change Permissions 174 | You can change a permission by specifying it as a four-tuple: (usertype, username, permissiontype, value). 175 | All other values remain unchanged. This function is not very efficient for multiple changes, 176 | since it requests a permission list from bamboo on every invocation. 177 | 178 | * The usertype takes values 'user', 'group' or 'other. 179 | * The username is the short username in bamboo. 180 | * The permissiontype is 'read', 'write', 'build', 'clone', 'admin', or 'all' to change all permissions for this user. 181 | * The value is either True or False. 182 | 183 | 184 | 185 | change_plan_permission(conn, 186 | plan_key, 187 | ('user','admin','all',True)) 188 | change_plan_permission(conn, 189 | plan_key, 190 | ('group','devel','build',True)) 191 | change_plan_permission(conn, 192 | plan_key, 193 | ('other','Anonymous Users','all',False)) 194 | 195 | ### Common Operations 196 | These are hints how to do common operations on results. They might not be optimal solutions. 197 | 198 | #### Delete a Number of Specific Tasks 199 | tasks_to_delete = [ 200 | 'my install task', 201 | 'my build task' 202 | ] 203 | for task_id, (task_key, description, _, _) in job_tasks.iteritems(): 204 | if description in tasks_to_delete: 205 | res = delete_job_task(conn, install_job_id, task_id) 206 | 207 | #### Add Multiple Requirements 208 | requirements = { 209 | "system.arch": "x86_64", 210 | "system.osfamily": "RedHat" 211 | } 212 | for req_key, req_value in requirements.iteritems(): 213 | res = add_job_requirement(conn, install_job_id, req_key, req_value) 214 | 215 | #### How To Get the Task Parameters 216 | Take Firefox with the firebug plugin and open the network tab. Make sure that you have 'persist' set, so connection details stay after page reloads. 217 | Add a task and look at the POST parameters. You don't have to specify all of them, the common ones are set by the function add _ job _ task(). If you're unsure, check the source. 218 | 219 | #### Add an Inline Script Task 220 | 221 | scriptBody = """""" 222 | 223 | cp_task_key = "com.atlassian.bamboo.plugins.scripttask:task.builder.script" 224 | cp_task_params = { 225 | "argument": "", 226 | "environmentVariables": "", 227 | "script": "", 228 | "scriptBody": scriptBody, 229 | "scriptLocation": "INLINE", 230 | "selectFields": "scriptLocation", 231 | "userDescription": "", 232 | "workingSubDirectory": "" 233 | } 234 | 235 | #### Get Plans from one Project 236 | 237 | project = 'MYPROJ' 238 | print 'PROJECT = %s' % project 239 | 240 | plan_list = get_plans(conn, 'plans.plan.actions') 241 | plan_keys = get_plan_keys(plan_list) 242 | print 'PLAN KEYS = %s' % project 243 | 244 | myproj_plan_keys = filter(lambda s: re.match(project,s) != None, plan_keys) 245 | print 'PLAN KEYS IN PROJECT %s = %s' % (project, myproj_plan_keys) 246 | 247 | -------------------------------------------------------------------------------- /dist/bamboo-automate.spec: -------------------------------------------------------------------------------- 1 | %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib())")} 2 | %{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib(1))")} 3 | 4 | %if 0%{?rhel} == 5 5 | %global with_python26 1 6 | %endif 7 | 8 | %if 0%{?with_python26} 9 | %global __python26 %{_bindir}/python2.6 10 | %global py26dir %{_builddir}/python26-%{name}-%{version}-%{release} 11 | %{!?python26_sitelib: %global python26_sitelib %(%{__python26} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib())")} 12 | %{!?python26_sitearch: %global python26_sitearch %(%{__python26} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib(1))")} 13 | # Update rpm byte compilation script so that we get the modules compiled by the 14 | # correct inerpreter 15 | %global __os_install_post %__multiple_python_os_install_post 16 | %endif 17 | 18 | Name: bamboo-automate 19 | Version: 0.1.0 20 | Release: 1%{?dist} 21 | BuildArch: noarch 22 | Summary: Python libraries to interact with a bamboo server 23 | Group: Applications/Internet 24 | License: ASL 2.0 25 | URL: https://github.com/mhellmic/bamboo-automate 26 | Source0: %{name}-%{version}.tar.gz 27 | Buildroot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) 28 | 29 | BuildRequires: cmake 30 | %if 0%{?with_python26} 31 | BuildRequires: python26-devel 32 | %else 33 | BuildRequires: python-devel 34 | %endif 35 | 36 | Requires: python-lxml 37 | 38 | %description 39 | This package provides a set of Python libraries to interact with an Atlassian Bamboo server. They use the bamboo REST API whenever possible, and otherwise go to the web frontend. 40 | 41 | %prep 42 | %setup -q -n %{name}-%{version} 43 | 44 | %build 45 | %cmake . -DCMAKE_INSTALL_PREFIX=/ 46 | make %{?_smp_mflags} 47 | 48 | %install 49 | rm -rf %{buildroot} 50 | mkdir -p %{buildroot} 51 | make install DESTDIR=%{buildroot} 52 | 53 | %clean 54 | rm -rf %{buildroot} 55 | 56 | %files 57 | %defattr(-,root,root,-) 58 | %{python_sitearch}/* 59 | 60 | -------------------------------------------------------------------------------- /lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | cmake_minimum_required (VERSION 2.6) 3 | 4 | install(PROGRAMS __init__.py 5 | bamboo_automate.py 6 | high_level_functions.py 7 | manipulate_bamboo_json.py 8 | prettyprint_functions.py 9 | requests.py 10 | DESTINATION ${PYTHON_SITE_PACKAGES}/bamboo_automate/ 11 | ) 12 | 13 | add_subdirectory(bamboo) 14 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | from bamboo_automate import * 2 | -------------------------------------------------------------------------------- /lib/bamboo/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.6) 2 | 3 | install(PROGRAMS __init__.py 4 | authenticate.py 5 | branches.py 6 | jobs.py 7 | permissions.py 8 | plans.py 9 | requirements.py 10 | results.py 11 | tasks.py 12 | variables.py 13 | DESTINATION ${PYTHON_SITE_PACKAGES}/bamboo_automate/bamboo/ 14 | ) 15 | -------------------------------------------------------------------------------- /lib/bamboo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhellmic/bamboo-automate/dbc7711f15874f907b6792897a4a8d4c1b65d62d/lib/bamboo/__init__.py -------------------------------------------------------------------------------- /lib/bamboo/agents.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .. import requests 3 | 4 | 5 | def get_agents(conn, sort_by_status=False): 6 | params = {} 7 | res = requests.get_ui_return_html( 8 | conn, 9 | conn.baseurl + '/agent/viewAgents.action', 10 | params) 11 | 12 | root = res # .getroot() 13 | 14 | agents = {} 15 | 16 | agent_idle_items = root.find_class('agentIdle') 17 | agent_building_items = root.find_class('agentBuilding') 18 | 19 | for tr in agent_idle_items + agent_building_items: 20 | name = tr.find('.//a').text 21 | status = tr.find('.//img').attrib['alt'].lower() 22 | if status.startswith('building'): 23 | status = 'building' 24 | 25 | if sort_by_status: 26 | s = agents.get(status, []) 27 | s.append(name) 28 | agents[status] = s 29 | else: 30 | agents[name] = status 31 | 32 | return agents 33 | -------------------------------------------------------------------------------- /lib/bamboo/authenticate.py: -------------------------------------------------------------------------------- 1 | import cookielib 2 | import logging 3 | import re 4 | from .. import requests 5 | import urllib2 6 | 7 | 8 | def _test_authentication(conn): 9 | """ Tests if the authentication was successful 10 | 11 | The first assumption is that the bamboo server uses an external 12 | authentication system. If the returned url is from the bamboo 13 | server, we assume authentication has succeeded, if it is not 14 | we assume we have been redirected to login again. 15 | The other hint we take into account is getting redirected to 16 | the bamboo login page again when bamboo-internal auth is used. 17 | 18 | This function is likely to give false positives in other 19 | deployments. 20 | 21 | """ 22 | page, url, http_code = requests.get_ui_return_html_status( 23 | conn, 24 | conn.baseurl, 25 | {}) 26 | if (re.search(conn.host, url) is not None and 27 | re.search('userlogin', url) is None): 28 | return True 29 | else: 30 | return False 31 | 32 | 33 | def external_authenticate(host, cookiefile, baseurl=''): 34 | retrieval_cookiejar = cookielib.MozillaCookieJar() 35 | retrieval_cookiejar.load(cookiefile, 36 | ignore_discard=True, 37 | ignore_expires=True) 38 | 39 | auth_cookies = [] 40 | for c in retrieval_cookiejar: 41 | if re.search(c.domain, host): 42 | if re.search('JSESSIONID', c.name): 43 | logging.debug('%s %s %s', c.domain, c.name, c.value) 44 | auth_cookies.append(c) 45 | if re.search('crowd.token_key', c.name): 46 | logging.debug('%s %s %s', c.domain, c.name, c.value) 47 | auth_cookies.append(c) 48 | 49 | cookiejar = cookielib.CookieJar() 50 | opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar)) 51 | conn = requests.Connection(host, baseurl, opener, cookiejar) 52 | conn.auth_cookies = auth_cookies 53 | 54 | if _test_authentication(conn): 55 | logging.debug('authentication test successful') 56 | conn.connected = True 57 | else: 58 | logging.debug('authentication test failed') 59 | conn.connected = False 60 | 61 | return conn 62 | 63 | 64 | def authenticate(host, user, passwd, baseurl=''): 65 | cookiejar = cookielib.CookieJar() 66 | opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar)) 67 | conn = requests.Connection(host, baseurl, opener, cookiejar) 68 | 69 | creds = { 70 | "os_username": user, 71 | "os_password": passwd 72 | } 73 | requests.post_ui_no_return( 74 | conn, 75 | conn.baseurl + '/userlogin!default.action', 76 | creds) 77 | 78 | if _test_authentication(conn): 79 | logging.debug('authentication test successful') 80 | conn.connected = True 81 | else: 82 | logging.debug('authentication test failed') 83 | conn.connected = False 84 | 85 | return conn 86 | -------------------------------------------------------------------------------- /lib/bamboo/branches.py: -------------------------------------------------------------------------------- 1 | from .. import requests 2 | from types import * 3 | 4 | def add_plan_branch(conn, plan_id, branch_name, branch_description=None): 5 | """ Add a branch to a plan. 6 | 7 | This function only creates the branch. 8 | It must be configured and enabled afterwards. 9 | 10 | The redirectUrl in the result contains the branch ID. 11 | 12 | """ 13 | assert type(branch_name) is StringType, 'branch_name is not type String: %r' % branch_name 14 | params = { 15 | "bamboo.successReturnMode": "json", 16 | "planKey": plan_id, 17 | "planKeyToClone": plan_id, 18 | "branchName": branch_name, 19 | "branchDescription": branch_description, 20 | "branchVcsFields": None, 21 | "checkBoxFields": "tmp.createAsEnabled", 22 | "confirm": "true", 23 | "creationOption": "MANUAL", 24 | "decorator": "nothing" 25 | } 26 | 27 | res = requests.post_ui_return_json( 28 | conn, 29 | conn.baseurl+'/chain/admin/createPlanBranch.action', 30 | params) 31 | 32 | return res 33 | 34 | def enable_plan_branch(conn, branch_id): 35 | params = { 36 | "enabled": "false", 37 | "branchName": "testbranch", 38 | "branchDescription": "mydesc", 39 | "branchConfiguration.cleanup.disabled": "true", 40 | "buildKey": branch_id, 41 | "checkBoxFields": "enabled", 42 | "checkBoxFields": "branchConfiguration.cleanup.disabled", 43 | "planKey": branch_id, 44 | "returnUrl": "", 45 | "save": "Save", 46 | } 47 | 48 | res = requests.post_ui_return_html( 49 | conn, 50 | conn.baseurl+'/branch/admin/config/saveChainBranchDetails.action', 51 | params) 52 | 53 | return res 54 | 55 | def mod_plan_branch_details(conn, branch_id, branch_params): 56 | """ Modify the branch details. 57 | 58 | The parameters must include: 59 | * branchDescription 60 | * branchName 61 | * enabled 62 | 63 | Arguments: 64 | conn -- the connection 65 | branch_id -- the unique ID of the branch 66 | branch_params -- the parameters in a dictionary 67 | 68 | """ 69 | params = { 70 | "branchConfiguration.cleanup.disabled": "true", 71 | "buildKey": branch_id, 72 | "checkBoxFields": "enabled", 73 | "checkBoxFields": "branchConfiguration.cleanup.disabled", 74 | "checkBoxFields": "overrideBuildStrategy", 75 | "checkBoxFields": "repositoryTrigger", 76 | "checkBoxFields": "custom.triggerrCondition.plansGreen.enabled", 77 | "planKey": branch_id, 78 | "returnUrl": "", 79 | "save": "Save", 80 | "selectFields": "selectedBuildStrategy", 81 | } 82 | params.update(branch_params) 83 | 84 | res = requests.post_ui_return_html( 85 | conn, 86 | conn.baseurl+'/branch/admin/config/saveChainBranchDetails.action', 87 | params) 88 | 89 | return res 90 | 91 | def delete_plan_branch(conn, branch_id): 92 | params = { 93 | "buildKey": branch_id, 94 | "save": "confirm" 95 | } 96 | 97 | res = requests.post_ui_return_html( 98 | conn, 99 | conn.baseurl+'/chain/admin/deleteChain!doDelete.action', 100 | params) 101 | 102 | return res 103 | 104 | def get_plan_branches(conn, plan_id, sort_by_title=False): 105 | """ Retrieve information about all branches of this plan. 106 | 107 | TODO: reimplement this to use the rest api. 108 | 109 | """ 110 | params = { 111 | "buildKey": plan_id 112 | } 113 | res = requests.get_ui_return_html( 114 | conn, 115 | conn.baseurl+'/chain/admin/config/editChainDetails.action', 116 | params) 117 | 118 | root = res #.getroot() 119 | 120 | branches = {} 121 | li_branches = root.findall('.//ul[@class="branches"]/li') 122 | for li in li_branches: 123 | key = None 124 | try: 125 | key = li.find('./a').attrib['id'].rsplit('_',1)[1] 126 | except: 127 | logging.error('no key for branch found.') 128 | edit_link = li.find('./a').attrib['href'] 129 | title = li.find('./a').text 130 | 131 | if sort_by_title: 132 | branches[title] = (key, edit_link,) 133 | else: 134 | branches[key] = (title, edit_link,) 135 | 136 | return branches 137 | -------------------------------------------------------------------------------- /lib/bamboo/jobs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .. import requests 3 | 4 | from collections import OrderedDict 5 | 6 | def get_jobs(conn, plan_id, sort_by_title=False): 7 | params = { 8 | "buildKey": plan_id 9 | } 10 | res = requests.get_ui_return_html( 11 | conn, 12 | conn.baseurl+'/chain/admin/config/editChainDetails.action', 13 | params) 14 | 15 | root = res #.getroot() 16 | 17 | jobs = OrderedDict() 18 | 19 | li_jobkeys = root.findall('.//li[@data-job-key]') 20 | for li in li_jobkeys: 21 | key = li.attrib['data-job-key'] 22 | edit_link = li.find('.//a').attrib['href'] 23 | del_link = None 24 | title = li.find('.//a').text 25 | description = None 26 | try: 27 | description = li.attrib['title'] 28 | except: 29 | pass 30 | 31 | if sort_by_title: 32 | jobs[title] = (key, description, edit_link, del_link,) 33 | else: 34 | jobs[key] = (title, description, edit_link, del_link,) 35 | 36 | return jobs 37 | 38 | def disable_job(conn, job_id, job_title): 39 | params = { 40 | "buildKey": job_id, 41 | "buildName": job_title, 42 | "checkBoxFields": "enabled", 43 | "save": "Save" 44 | } 45 | res = requests.get_ui_return_html( 46 | conn, 47 | conn.baseurl+'/build/admin/edit/updateBuildDetails.action', 48 | params) 49 | 50 | return res 51 | 52 | def enable_job(conn, job_id, job_title): 53 | params = { 54 | "buildKey": job_id, 55 | "buildName": job_title, 56 | "checkBoxFields": "enabled", 57 | "enabled": "true", 58 | "save": "Save" 59 | } 60 | res = requests.get_ui_return_html( 61 | conn, 62 | conn.baseurl+'/build/admin/edit/updateBuildDetails.action', 63 | params) 64 | 65 | return res; 66 | -------------------------------------------------------------------------------- /lib/bamboo/permissions.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .. import requests 3 | 4 | def _check_permission(html_root, usertype, username, permission): 5 | if usertype == 'other': 6 | usertype = 'role' 7 | if username == 'Logged in Users': 8 | username = 'ROLE_USER' 9 | elif username == 'Anonymous Users': 10 | username = 'ROLE_ANONYMOUS' 11 | permission_input_field_name = 'bambooPermission_'+usertype+'_'+username+'_'+permission.upper() 12 | permission_cell_name = permission_input_field_name+'_cell' 13 | permission_xpath = './/td[@id="'+permission_cell_name+'"]/input[@name="'+permission_input_field_name+'"]' 14 | logging.debug('xpath to search for permission checkbox = %s' % permission_xpath) 15 | el = html_root.find(permission_xpath) 16 | if el == None: 17 | logging.debug('element not found') 18 | return False 19 | logging.debug('element is checked = %s', True if 'checked' in el.attrib else False) 20 | if 'checked' in el.attrib: 21 | return True 22 | else: 23 | return False 24 | 25 | def _get_type_permissions(html_root, usertype): 26 | table_user = html_root.findall('.//table[@id="configureBuild'+usertype.capitalize()+'Permissions"]/tr') 27 | logging.debug('xpath to search for permission table = %s' % table_user) 28 | 29 | user_permissions = {} 30 | 31 | for tr in table_user: 32 | key = None 33 | try: 34 | key = tr.find('td[1]/a').attrib['href'].rsplit('/',1)[1] 35 | except: 36 | key = tr.find('td[1]').text 37 | read_p = _check_permission(tr, usertype, key, 'READ') 38 | write_p = _check_permission(tr, usertype, key, 'WRITE') 39 | build_p = _check_permission(tr, usertype, key, 'BUILD') 40 | clone_p = _check_permission(tr, usertype, key, 'CLONE') 41 | admin_p = _check_permission(tr, usertype, key, 'ADMINISTRATION') 42 | 43 | user_permissions[key] = {'read':read_p, 44 | 'write':write_p, 45 | 'build':build_p, 46 | 'clone':clone_p, 47 | 'admin':admin_p} 48 | 49 | return user_permissions 50 | 51 | def get_plan_permissions(conn, plan_id): 52 | params = { 53 | "buildKey": plan_id 54 | } 55 | res = requests.get_ui_return_html( 56 | conn, 57 | conn.baseurl+'/chain/admin/config/editChainPermissions.action', 58 | params) 59 | 60 | root = res #.getroot() 61 | 62 | user_permissions = _get_type_permissions(root, 'user') 63 | group_permissions = _get_type_permissions(root, 'group') 64 | other_permissions = _get_type_permissions(root, 'other') 65 | 66 | return {'user': user_permissions, 67 | 'group': group_permissions, 68 | 'other': other_permissions} 69 | 70 | 71 | def mod_plan_permissions(conn, plan_id, permission_params): 72 | params = { 73 | "buildKey": plan_id, 74 | "newGroup": None, 75 | "newUser": None, 76 | "principalType": "User", 77 | "save": "Save", 78 | "selectFields": "principalType" 79 | } 80 | params.update(permission_params) 81 | res = requests.post_ui_return_html( 82 | conn, 83 | conn.baseurl+'/chain/admin/config/updateChainPermissions.action', 84 | params) 85 | 86 | return res 87 | -------------------------------------------------------------------------------- /lib/bamboo/plans.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .. import requests 3 | 4 | def _iterate_json_entity_results(request, conn, entity, path, params): 5 | start_index = 0 6 | params.update({ 7 | "start-index": start_index 8 | }) 9 | entities = entity+'s' 10 | res = request(conn, path, params) 11 | logging.debug('%s', res[entities]['max-result']) 12 | part_result_size = res[entities]['max-result'] 13 | result_size = res[entities]['size'] 14 | part_res = res 15 | while start_index <= result_size: 16 | logging.debug('size = %s max-result = %s', res[entities]['size'], res[entities]['max-result']) 17 | logging.debug('start_index = %s', start_index) 18 | start_index = start_index + part_result_size 19 | params.update({ 20 | "start-index": start_index 21 | }) 22 | part_res = request(conn, path, params) 23 | res[entities][entity].extend(part_res[entities][entity]) 24 | 25 | return res 26 | 27 | def _get_entity(conn, entity, expand): 28 | params = { 29 | "expand": expand 30 | } 31 | res = _iterate_json_entity_results( 32 | requests.get_rest_return_json, 33 | conn, 34 | entity, 35 | conn.baseurl+'/rest/api/latest/'+entity, 36 | params) 37 | 38 | return res 39 | 40 | def get_plans(conn, expand=''): 41 | return _get_entity(conn, 'plan', expand) 42 | 43 | def get_projects(conn, expand=''): 44 | return _get_entity(conn, 'project', expand) 45 | -------------------------------------------------------------------------------- /lib/bamboo/requirements.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .. import requests 3 | import re 4 | 5 | def _get_requirements(conn, job_id): 6 | params = { 7 | "buildKey": job_id 8 | } 9 | res = requests.get_ui_return_html( 10 | conn, 11 | conn.baseurl+'/build/admin/edit/defaultBuildRequirement.action', 12 | params) 13 | 14 | root = res #.getroot() 15 | 16 | requirements = {} 17 | 18 | td_labels = root.find_class('labelCell') 19 | for td in td_labels: 20 | key = None 21 | req_id = None 22 | edit_link = None 23 | del_link = None 24 | tr = td.getparent() 25 | links = tr.findall('.//a') 26 | for l in links: 27 | href = l.attrib['href'] 28 | match = re.search('capabilityKey=(.*)', href) 29 | if match: 30 | key = match.group(1) 31 | match = re.search('editBuildRequirement.*requirementId=(\d+)', href) 32 | if match: 33 | edit_link = href 34 | req_id = match.group(1) 35 | match = re.search('deleteBuildRequirement.*requirementId=(\d+)', href) 36 | if match: 37 | del_link = href 38 | req_id = match.group(1) 39 | 40 | if not key: 41 | key = td.text.strip() 42 | 43 | requirements[key] = (req_id, edit_link, del_link,) 44 | 45 | return requirements 46 | 47 | def delete_job_requirement(conn, job_id, req_key): 48 | requirements = _get_requirements(conn, job_id) 49 | logging.debug('%s', requirements) 50 | res = None 51 | req_id, _, del_link = requirements[req_key] 52 | if req_id != None: 53 | res = requests.post_ui_no_return(conn, del_link, {}) 54 | 55 | return res 56 | 57 | def delete_job_all_requirements(conn, job_id): 58 | requirements = _get_requirements(conn, job_id) 59 | res = None 60 | for req_id, _, del_link in requirements.itervalues(): 61 | if req_id != None: 62 | res = requests.post_ui_no_return(conn, del_link, {}) 63 | 64 | return res 65 | 66 | def add_job_requirement(conn, job_id, req_key, req_value, req_exists=False): 67 | params = { 68 | "Add": "Add", 69 | "buildKey": job_id, 70 | "existingRequirement": req_key if req_exists else None, 71 | "regexMatchValue": None, 72 | "requirementKey": None if req_exists else req_key, 73 | "requirementMatchType": "equal", 74 | "requirementMatchValue": req_value, 75 | "selectFields": "existingRequirement", 76 | "selectFields": "requirementMatchType" 77 | } 78 | logging.debug(params) 79 | res = requests.post_ui_return_html( 80 | conn, 81 | conn.baseurl+'/build/admin/edit/addBuildRequirement.action', 82 | params) 83 | 84 | return res 85 | -------------------------------------------------------------------------------- /lib/bamboo/results.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .. import requests 3 | 4 | def get_build_results(conn, project_key=None, expand=''): 5 | params = { 6 | "expand": expand 7 | } 8 | entity = "result/"+project_key if project_key else "result" 9 | res = requests.get_rest_return_json( 10 | conn, 11 | conn.baseurl+'/rest/api/latest/'+entity, 12 | params) 13 | 14 | return res 15 | 16 | -------------------------------------------------------------------------------- /lib/bamboo/tasks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .. import requests 3 | import re 4 | 5 | from collections import OrderedDict 6 | 7 | def get_tasks(conn, job_id, sort_by_title=False): 8 | params = { 9 | "buildKey": job_id 10 | } 11 | res = requests.get_ui_return_html( 12 | conn, 13 | conn.baseurl+'/build/admin/edit/editBuildTasks.action', 14 | params) 15 | 16 | root = res #.getroot() 17 | 18 | tasks = OrderedDict() 19 | 20 | li_items = root.find_class('item') 21 | for order_id, li in enumerate(li_items, start=1): 22 | key = int(li.attrib['data-item-id']) 23 | edit_link = None 24 | del_link = None 25 | title = li.find('.//h3').text 26 | description = None 27 | try: 28 | description = li.find('.//div').text 29 | except: 30 | pass 31 | links = li.findall('.//a') 32 | for l in links: 33 | href = l.attrib['href'] 34 | match = re.search('editTask', href) 35 | if match: 36 | edit_link = href 37 | match = re.search('confirmDeleteTask', href) 38 | if match: 39 | del_link = href 40 | req_id = href 41 | 42 | if sort_by_title: 43 | title_desc = (title, description) 44 | tasks[title_desc] = (key, (title, description), edit_link, del_link, order_id,) 45 | else: 46 | tasks[key] = (title, description, edit_link, del_link, order_id,) 47 | 48 | return tasks 49 | 50 | def add_job_task(conn, job_id, task_id, task_params): 51 | params = { 52 | "bamboo.successReturnMode": "json", 53 | "planKey": job_id, 54 | "checkBoxFields": "taskDisabled", 55 | "confirm": "true", 56 | "createTaskKey": task_id, 57 | "decorator": "nothing", 58 | "taskId": 0, 59 | "finalising": "true", 60 | "userDescription": None 61 | } 62 | params.update(task_params) 63 | res = requests.post_ui_return_json( 64 | conn, 65 | conn.baseurl+'/build/admin/edit/createTask.action', 66 | params) 67 | 68 | return res 69 | 70 | def delete_job_task(conn, job_id, task_id): 71 | params = { 72 | "bamboo.successReturnMode": "json", 73 | "planKey": job_id, 74 | "confirm": "true", 75 | "createTaskKey": None, 76 | "decorator": "nothing", 77 | "taskId": task_id 78 | } 79 | res = requests.post_ui_return_json( 80 | conn, 81 | conn.baseurl+'/build/admin/edit/deleteTask.action', 82 | params) 83 | 84 | return res 85 | 86 | def move_job_task(conn, job_id, task_id, finalising=False, beforeId=None, afterId=None): 87 | """ Move a task in the runtime order. 88 | 89 | Arguments: 90 | conn -- the connection object 91 | job_id -- the id of the job 92 | task_id -- the id of the task to move 93 | finalising -- true, if task should be a final task 94 | beforeId -- id of the task which should be before this task 95 | afterId -- id of the task which should be after this taks 96 | 97 | """ 98 | params = { 99 | "planKey": job_id, 100 | "finalising": "true" if finalising else "false", 101 | "taskId": task_id 102 | } 103 | if beforeId: 104 | params.update({"beforeId": beforeId}) 105 | if afterId: 106 | params.update({"afterId": afterId}) 107 | res = requests.post_ui_return_json( 108 | conn, 109 | conn.baseurl+'/build/admin/ajax/moveTask.action', 110 | params) 111 | logging.debug(params) 112 | 113 | return res 114 | -------------------------------------------------------------------------------- /lib/bamboo/variables.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .. import requests 3 | import re 4 | 5 | def add_plan_variable(conn, plan_id, var_key, var_value): 6 | params = { 7 | "planKey": plan_id, 8 | "variableKey": var_key, 9 | "variableValue": var_value 10 | } 11 | res = requests.post_ui_return_json( 12 | conn, 13 | conn.baseurl+'/build/admin/ajax/createPlanVariable.action', 14 | params) 15 | 16 | return res 17 | 18 | def mod_plan_variable(conn, plan_id, var_key, var_value): 19 | var_id = _find_variable_id(conn, plan_id, var_key) 20 | logging.debug('%s', var_id) 21 | 22 | params = { 23 | "planKey": plan_id, 24 | "variableId": var_id, 25 | "variableKey": var_key, 26 | "variableValue": var_value 27 | } 28 | res = requests.post_ui_return_json( 29 | conn, 30 | conn.baseurl+'/build/admin/ajax/updatePlanVariable.action', 31 | params) 32 | 33 | return res 34 | 35 | def add_mod_plan_variable(conn, plan_id, var_key, var_value): 36 | res = add_plan_variable(conn, plan_id, var_key, var_value) 37 | if res['status'] == 'ERROR': 38 | logging.debug(res['fieldErrors']['variableKey']) 39 | if 'This plan already contains a variable named '+var_key in res['fieldErrors']['variableKey']: 40 | res = mod_plan_variable(conn, plan_id, var_key, var_value) 41 | 42 | return res 43 | 44 | def delete_plan_variable(conn, plan_id, var_key): 45 | var_id = _find_variable_id(conn, plan_id, var_key) 46 | logging.debug('%s', var_id) 47 | 48 | params = { 49 | "planKey": plan_id, 50 | "variableId": var_id 51 | } 52 | res = requests.post_ui_return_json( 53 | conn, 54 | conn.baseurl+'/build/admin/ajax/deletePlanVariable.action', 55 | params) 56 | 57 | return res 58 | 59 | def _delete_plan_variable_by_id(conn, plan_id, var_id): 60 | logging.debug('%s', var_id) 61 | 62 | params = { 63 | "bamboo.successReturnMode": "json", 64 | "confirm": "true", 65 | "decorator": "nothing", 66 | "planKey": plan_id, 67 | "variableId": var_id 68 | } 69 | res = requests.post_ui_return_json( 70 | conn, 71 | conn.baseurl+'/build/admin/ajax/deletePlanVariable.action', 72 | params) 73 | 74 | return res 75 | 76 | def delete_plan_all_variables(conn, plan_id): 77 | variables = _find_all_variables(conn, plan_id) 78 | for var_id in variables.itervalues(): 79 | logging.debug(var_id) 80 | res = _delete_plan_variable_by_id(conn, plan_id, var_id) 81 | 82 | def _find_all_variables(conn, plan_id): 83 | params = { 84 | "buildKey": plan_id 85 | } 86 | res = requests.get_ui_return_html( 87 | conn, 88 | conn.baseurl+'/chain/admin/config/configureChainVariables.action', 89 | params) 90 | 91 | variables = {} 92 | 93 | match_key = re.compile('key_(-?\d+)') 94 | root = res #.getroot() 95 | span_varid = root.find_class('inline-edit-field text') 96 | for v in span_varid: 97 | m = match_key.match(v.name) 98 | if m: 99 | variables[v.value] = m.group(1) 100 | 101 | return variables 102 | 103 | def _find_variable_id(conn, plan_id, var_key): 104 | variables = _find_all_variables(conn, plan_id) 105 | logging.debug('all variables:\n'+str(variables)) 106 | return variables[var_key] 107 | 108 | -------------------------------------------------------------------------------- /lib/bamboo_automate.py: -------------------------------------------------------------------------------- 1 | from high_level_functions import * 2 | from manipulate_bamboo_json import * 3 | from prettyprint_functions import * 4 | 5 | from bamboo.agents import * 6 | from bamboo.authenticate import * 7 | from bamboo.branches import * 8 | from bamboo.jobs import * 9 | from bamboo.permissions import * 10 | from bamboo.plans import * 11 | from bamboo.requirements import * 12 | from bamboo.results import * 13 | from bamboo.tasks import * 14 | from bamboo.variables import * 15 | 16 | logging.basicConfig(level=logging.DEBUG) 17 | -------------------------------------------------------------------------------- /lib/high_level_functions.py: -------------------------------------------------------------------------------- 1 | from manipulate_bamboo_json import * 2 | 3 | from bamboo.authenticate import * 4 | from bamboo.branches import * 5 | from bamboo.jobs import * 6 | from bamboo.permissions import * 7 | from bamboo.plans import * 8 | from bamboo.requirements import * 9 | from bamboo.tasks import * 10 | from bamboo.variables import * 11 | 12 | import re 13 | from types import * 14 | 15 | def print_result(res, cmd, key): 16 | print '%(cmd)s %(key)s %(stat)s' % {'cmd': cmd, 'key': key, 17 | 'stat': 'SUCCESS' if res['status'] == 'OK' else 'FAILED'} 18 | 19 | def print_result_debug(res, cmd, key): 20 | logging.debug('%(cmd)s %(key)s %(stat)s' % {'cmd': cmd, 'key': key, 21 | 'stat': 'SUCCESS' if res['status'] == 'OK' else 'FAILED'}) 22 | 23 | def change_plan_permission(conn, plan_key, permission): 24 | """ Change a single permission for a plan. 25 | 26 | Changes a permission provided its description as a three-tuple 27 | (usertype, username, permissiontype, value). 28 | It gets the current permissions on the plan, changes the permission 29 | and updates the plan. 30 | Note that _all_ permissions have to be send to bamboo, not only the 31 | changing ones. This function provides a safe interface. 32 | 33 | Since the function receives the current on every invocation, it is not 34 | efficient to use for changing many permissions. 35 | 36 | """ 37 | assert type(permission) is TupleType, 'permission argument is not a tuple: %(t)r' % {'t':permission} 38 | assert len(permission) == 4, 'permission tuple does not have four values: %(t)r' % {'t':permission} 39 | assert type(permission[0]) is StringType, 'permission tuple\'s first value is not type string: %(t)r' % {'t':permission[0]} 40 | assert type(permission[1]) is StringType, 'permission tuple\'s second value is not type string: %(t)r' % {'t':permission[1]} 41 | assert type(permission[2]) is StringType, 'permission tuple\'s third value is not type string: %(t)r' % {'t':permission[2]} 42 | assert type(permission[3]) is BooleanType, 'permission tuple\'s fourth value is not type bool: %(t)r' % {'t':permission[3]} 43 | 44 | usertype, username, permissiontype, value = permission 45 | permissions = get_plan_permissions(conn, plan_key) 46 | try: 47 | if permissiontype == 'all': 48 | for key in permissions[usertype][username].iterkeys(): 49 | permissions[usertype][username][key] = value 50 | else: 51 | permissions[usertype][username][permissiontype] = value 52 | except KeyError: 53 | logging.info('Could not change permission: %(t)r' % {'t':permission}) 54 | return 55 | 56 | mod_plan_permissions( 57 | conn, 58 | plan_key, 59 | parse_permission_params(permissions)) 60 | 61 | 62 | def get_plans_in_project(conn, project_key, exclude_regex=None): 63 | """ Get all plans which belong to a project. 64 | 65 | This function updates the list of plans and filters it according 66 | to the provided project key. It can additionally filter the plan keys 67 | with the regex argument. 68 | 69 | Arguments: 70 | conn -- the connection 71 | project_key -- the project key to filter 72 | exclude_regex -- filter out plan with names that match this regex 73 | 74 | """ 75 | plans = get_plans(conn, expand='plans.plan') 76 | project_plans = filter(lambda d:d['projectKey'] == project_key, 77 | plans['plans']['plan']) 78 | 79 | try: 80 | exclude_regex = re.compile(exclude_regex) 81 | filtered_project_plans = filter(lambda d:exclude_regex.search(d['shortKey']) == None, project_plans) 82 | except: 83 | filtered_project_plans = project_plans 84 | 85 | plans['plans']['plan'] = filtered_project_plans 86 | return plans 87 | 88 | def move_task_to_position(conn, job_key, task_key, pos=None, finalising=False): 89 | """ Reorder a task list. 90 | 91 | This function either moves a task to a given position or puts it last 92 | in the finalized section. A specific position in the finalized section 93 | cannot be chosen. 94 | 95 | Moving to the last position can be specified with pos=-1. 96 | 97 | Arguments: 98 | conn -- the connection 99 | job_key -- the job key 100 | task_key -- the key of the task to be positioned 101 | pos -- the position to put it, zero-indexed 102 | 103 | """ 104 | assert pos != None or finalising, 'you must provide either the pos or finalising == True' 105 | 106 | res = None 107 | tasks = None 108 | task_id, tasks = get_task_id_and_dict(conn, job_key, task_key) 109 | 110 | task_list = task_dict_to_list(tasks) 111 | task_id_before = None 112 | task_id_after = None 113 | 114 | if pos == -1 or finalising: 115 | if len(task_list) > 0: 116 | task_id_last = task_list[-1].task_id 117 | # the last id may be our task we want to move 118 | if len(task_list) <= 1: 119 | task_id_last = None 120 | elif task_id_last == task_id: 121 | task_id_last = task_list[-2].task_id 122 | logging.debug('last task id = %(id)s' % {'id':task_id_last}) 123 | res = move_job_task(conn, job_key, task_id, finalising=finalising, beforeId=task_id_last) 124 | return res 125 | 126 | try: 127 | if pos > 0: 128 | task_id_before = task_list[pos-1].task_id 129 | assert type(task_id_before) is IntType, 'task_id_before is not an int: %r' % task_id_before 130 | except: 131 | pass 132 | try: 133 | task_id_after = task_list[pos].task_id 134 | assert type(task_id_before) is IntType, 'task_id_before is not an int: %r' % task_id_before 135 | except: 136 | pass 137 | 138 | if task_id_before and task_id_after: 139 | res = move_job_task(conn, job_key, task_id, beforeId=task_id_before, afterId=task_id_after) 140 | elif task_id_before: 141 | res = move_job_task(conn, job_key, task_key, beforeId=task_id_before) 142 | elif task_id_after: 143 | res = move_job_task(conn, job_key, task_key, afterId=task_id_after) 144 | else: 145 | print 'ERROR: moving task %(task)s to position %(pos)s failed.' % {'task': task_key, 'pos': pos } 146 | 147 | return res 148 | 149 | def _get_id_and_dict(conn, outer_key, inner_key, get_inner_func, inner_key_name): 150 | inner_id = None 151 | inner_dict = {} 152 | try: 153 | int(inner_key) # it's the ID, not the title 154 | inner_dict = get_inner_func(conn, outer_key) 155 | if inner_key in inner_dict: 156 | inner_id = inner_key 157 | except: # it's the title 158 | inner_dict = get_inner_func(conn, outer_key, sort_by_title=True) 159 | if inner_key in inner_dict: 160 | inner_id = inner_dict[inner_key][0] 161 | 162 | return inner_id, inner_dict 163 | 164 | 165 | def get_task_id_and_dict(conn, job_key, task_key): 166 | """ Get the task id from a task key and a dict with all tasks in the job 167 | 168 | The task key can be the id or the title. This function determines this, 169 | then downloads the appropriate task dict and resolves to the id. 170 | 171 | """ 172 | return _get_id_and_dict(conn, job_key, task_key, get_tasks, 'task') 173 | 174 | def get_job_id_and_dict(conn, plan_key, job_key): 175 | """ Get the job id from a job key and a dict with all jobs in the job 176 | 177 | The job key can be the id or the title. This function determines this, 178 | then downloads the appropriate job dict and resolves to the id. 179 | 180 | """ 181 | return _get_id_and_dict(conn, plan_key, job_key, get_jobs, 'job') 182 | 183 | def delete_task(conn, plan_key, job_title, task_key): 184 | """ Deletes a task from one job. 185 | 186 | This function determines the job id to the given job_title, 187 | the finds the task id from the task_key, and tries to delete it. 188 | 189 | """ 190 | job_id, _ = get_job_id_and_dict(conn, plan_key, job_title) 191 | logging.debug('JOB ID = %(job_id)s' % {'job_id': job_id,}) 192 | task_id, _ = get_task_id_and_dict(conn, job_id, task_key) 193 | logging.debug('TASK ID = %(task_id)s' % {'task_id': task_id,}) 194 | 195 | try: 196 | int(task_id) 197 | res = delete_job_task(conn, job_id, task_id) 198 | except: 199 | res = {'status':'OK'} 200 | 201 | return res 202 | 203 | def insert_task(conn, plan_key, job_title, task_key, task_params, position=None, finalising=False): 204 | job_id, _ = get_job_id_and_dict(conn, plan_key, job_title) 205 | logging.debug('JOB ID = %(job_id)s' % {'job_id': job_id,}) 206 | res = add_job_task(conn, job_id, task_key, task_params) 207 | print_result_debug(res, 'adding', task_key) 208 | task_id = res['taskResult']['task']['id'] 209 | logging.debug('TASK ID = %(task_id)s' % {'task_id': task_id,}) 210 | 211 | if position != None or finalising == True: 212 | res = move_task_to_position(conn, job_id, task_id, position, finalising) 213 | print_result_debug(res, 'positioning', task_key) 214 | 215 | return res 216 | 217 | -------------------------------------------------------------------------------- /lib/manipulate_bamboo_json.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | Task = namedtuple('Task', 'task_id title desc edit_link, del_link, order_id') 4 | 5 | def _get_value_from_bamboo_dict(bamboo_dict, dict_type, type_value): 6 | entity_list = bamboo_dict[dict_type+'s'][dict_type] 7 | return map(lambda d: d[type_value], entity_list) 8 | 9 | def get_keys(bamboo_dict, dict_type): 10 | return _get_value_from_bamboo_dict(bamboo_dict, 11 | dict_type, 12 | 'key') 13 | def get_project_keys(bamboo_dict): 14 | return _get_value_from_bamboo_dict(bamboo_dict, 15 | 'project', 16 | 'key') 17 | 18 | def get_plan_keys(bamboo_dict): 19 | return _get_value_from_bamboo_dict(bamboo_dict, 20 | 'plan', 21 | 'key') 22 | 23 | def order_tasks_in_list(list_dict): 24 | res_list = [] 25 | for key, value in list_dict.iteritems(): 26 | task = None 27 | try: 28 | int(key) 29 | task = Task(key, value[0], value[1], value[2], value[3], value[4]) 30 | except: 31 | task = Task(value[0], value[1][0], value[1][1], value[2], value[3], value[4]) 32 | 33 | res_list.append(task) 34 | 35 | return sorted(res_list, key=lambda t: t.task_id) 36 | 37 | def task_dict_to_list(list_dict): 38 | res_list = [] 39 | for key, value in list_dict.iteritems(): 40 | task = None 41 | try: 42 | int(key) 43 | task = Task(key, value[0], value[1], value[2], value[3], value[4]) 44 | except: 45 | task = Task(value[0], value[1][0], value[1][1], value[2], value[3], value[4]) 46 | 47 | res_list.append(task) 48 | 49 | return res_list 50 | 51 | def _correct_permission_usertype(usertype): 52 | if usertype == 'other': 53 | return 'role' 54 | else: 55 | return usertype 56 | 57 | def _correct_permission_username(username): 58 | if username == 'Logged in Users': 59 | return 'ROLE_USER' 60 | elif username == 'Anonymous Users': 61 | return 'ROLE_ANONYMOUS' 62 | else: 63 | return username 64 | 65 | def _correct_permission_type(ptype): 66 | if ptype == 'admin': 67 | return 'administration' 68 | else: 69 | return ptype 70 | 71 | def parse_permission_params(params_dict): 72 | params = {} 73 | for pusertype, pusertype_val in params_dict.iteritems(): 74 | usertype = _correct_permission_usertype(pusertype) 75 | for puser, pperms in pusertype_val.iteritems(): 76 | username = _correct_permission_username(puser) 77 | for ptype, pval in pperms.iteritems(): 78 | permtype = _correct_permission_type(ptype) 79 | if pval: 80 | perm_string = '_'.join(['bambooPermission', 81 | usertype, 82 | username, 83 | permtype.upper()]) 84 | params[perm_string] = 'on' 85 | 86 | return params 87 | 88 | -------------------------------------------------------------------------------- /lib/prettyprint_functions.py: -------------------------------------------------------------------------------- 1 | 2 | def print_build_results(res, filter_func=lambda x: True): 3 | build_list = res['results']['result'] 4 | print '==================================' 5 | print ' Build results:' 6 | print ' key\tstate\tlink' 7 | print '==================================' 8 | for build in filter(filter_func, build_list): 9 | print ' %(build_key)s\t%(build_state)s\t%(build_link)s' % { 10 | 'build_key': build['key'], 11 | 'build_state': build['state'], 12 | 'build_link': build['link']['href'] 13 | } 14 | 15 | def print_ls(l): 16 | for item in l: 17 | print item 18 | -------------------------------------------------------------------------------- /lib/requests.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from lxml import html 3 | import json 4 | import logging 5 | import re 6 | import time 7 | import urllib 8 | import urllib2 9 | 10 | 11 | MAXRETRIES = 3 12 | RETRYSLEEP = 5 13 | 14 | 15 | class Connection: 16 | def __init__(self, hostname, baseurl, opener, auth_cookies): 17 | self.host = hostname 18 | self.opener = opener 19 | self.auth_cookies = auth_cookies 20 | self.baseurl = baseurl 21 | self.connected = False 22 | 23 | def get_ui_return_html_status(conn, path, params): 24 | page, url, http_code = _request(conn, "GET", path, params, [], urllib.urlencode, html.fromstring) 25 | return page, url, http_code 26 | 27 | def get_ui_return_html(conn, path, params): 28 | res, _, _ = _request(conn, "GET", path, params, [], urllib.urlencode, html.fromstring) 29 | return res 30 | 31 | def get_ui_return_json(conn, path, params): 32 | headers = [('Accept', 'application/json')] 33 | try: 34 | res, _, _ = _request(conn, "GET", path, params, headers, urllib.urlencode, json.loads) 35 | except: 36 | res = {'status':'NotOK'} 37 | return res 38 | 39 | def post_ui_no_return(conn, path, params): 40 | res,_ , _ = _request(conn, "POST", path, params, [], urllib.urlencode, id) 41 | 42 | def post_ui_return_html(conn, path, params): 43 | res, _, _ = _request(conn, "POST", path, params, [], urllib.urlencode, html.fromstring) 44 | return res 45 | 46 | def post_ui_return_json(conn, path, params): 47 | headers = [('Accept', 'application/json')] 48 | try: 49 | res, _, _ = _request(conn, "POST", path, params, headers, urllib.urlencode, json.loads) 50 | except: 51 | res = {'status':'NotOK'} 52 | return res 53 | 54 | def get_rest_return_json(conn, path, params): 55 | return get_ui_return_json(conn, path, params) 56 | 57 | def post_rest_return_json(conn, path, params): 58 | return get_ui_return_json(conn, path, params) 59 | 60 | rolling_avg_counter = 1 61 | rolling_avg_list = [] 62 | rolling_avg = 0 63 | def monitoring(func): 64 | @wraps(func) 65 | def with_time_monitoring(*args, **kwargs): 66 | global rolling_avg_counter, rolling_avg_list, rolling_avg 67 | t1 = time.time() 68 | time.sleep(3.0) 69 | t2 = time.time() 70 | logging.debug('slept %(sleep)s seconds.' % {'sleep': (t2 - t1)}) 71 | start_time = time.time() 72 | res = func(*args, **kwargs) 73 | end_time = time.time() 74 | logging.debug('%(fname)s took %(dur)s.' % {'fname': func.__name__, 75 | 'dur': (end_time - start_time)}) 76 | 77 | rolling_avg_counter += 1 78 | rolling_avg_list.append(end_time - start_time) 79 | if rolling_avg_counter % 10 == 0: 80 | rolling_avg_counter = 1 81 | rolling_avg = ((rolling_avg + sum(rolling_avg_list)) / 82 | (1 + len(rolling_avg_list))) 83 | rolling_avg_list = [] 84 | logging.debug('%(fname)s rolling average is %(dur)s.' 85 | % {'fname': func.__name__, 86 | 'dur': rolling_avg}) 87 | 88 | return res 89 | return with_time_monitoring 90 | 91 | @monitoring 92 | def _request(conn, method, path, params, headers, param_parse_func, response_parse_func): 93 | path_and_params = None 94 | if method == "GET": 95 | path_and_params = path+'?'+urllib.urlencode(params) if params else path 96 | else: 97 | path_and_params = path 98 | 99 | req = urllib2.Request(conn.host+path_and_params) 100 | for key, value in headers: 101 | req.add_header(key, value) 102 | 103 | cookies = [] 104 | for c in conn.auth_cookies: 105 | cookies.append(c.name+'='+c.value.strip()) 106 | 107 | cookies = '; '.join(cookies) 108 | if len(cookies) > 0: 109 | req.add_header('Cookie', cookies) 110 | 111 | retries = 0 112 | req_success = False 113 | while not req_success: 114 | logging.debug('%s', req.get_full_url()) 115 | try: 116 | if method == "POST": 117 | response = conn.opener.open(req, param_parse_func(params)) 118 | elif method == "GET": 119 | response = conn.opener.open(req) 120 | req_success = True 121 | except urllib2.URLError: 122 | if retries >= MAXRETRIES: 123 | raise 124 | time.sleep(RETRYSLEEP) 125 | finally: 126 | retries += 1 127 | 128 | logging.debug('%s %s', response.geturl(), response.getcode()) 129 | if response.getcode() > 399: 130 | raise urllib2.HTTPError(code=response.getcode()) 131 | 132 | response_content = response.read() 133 | try: 134 | res = response_parse_func(response_content) 135 | except: 136 | logging.debug('The response content follows:') 137 | logging.debug(response_content) 138 | logging.debug('End of response content.') 139 | raise 140 | 141 | return res, response.geturl(), response.getcode() 142 | --------------------------------------------------------------------------------