├── tests ├── inventory └── test.yml ├── meta └── main.yml ├── README.md ├── library ├── artifactory_groups.py ├── artifactory_users.py ├── artifactory_permissions.py ├── artifactory_replication.py └── artifactory_repo.py └── lib └── ansible └── module_utils └── artifactory.py /tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | 3 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: Kyle Haley 3 | description: Collection of modules for managing JFrog Artifactory 4 | company: Rackspace 5 | 6 | license: GPLv3 7 | 8 | min_ansible_version: 1.2 9 | 10 | platforms: 11 | - name: GenericLinux 12 | versions: all 13 | 14 | galaxy_tags: 15 | - jfrog 16 | - artifactory 17 | - packaging 18 | - packages 19 | - repository 20 | 21 | dependencies: [] 22 | -------------------------------------------------------------------------------- /tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | vars_prompt: 4 | - name: artifactory_url 5 | prompt: "What is the URL of your artifactory?" 6 | - name: artifactory_user 7 | prompt: "What is your artifactory user?" 8 | - name: artifactory_password 9 | prompt: "What your artifactory password?" 10 | private: yes 11 | - name: artifactory_auth_token 12 | prompt: "What your artifactory auth_token?" 13 | private: yes 14 | roles: 15 | - artifactory 16 | tasks: 17 | - name: create test-local-creation repo 18 | artifactory_repo: 19 | artifactory_url: "{{ artifactory_url }}" 20 | auth_token: "{{ artifactory_auth_token }}" 21 | repo: "test-local-creation" 22 | state: present 23 | repo_config: '{"rclass": "local"}' 24 | 25 | - name: Delete a local repository in artifactory with auth_token 26 | artifactory_repo: 27 | artifactory_url: "{{ artifactory_url }}" 28 | auth_token: "{{ artifactory_auth_token }}" 29 | repo: "test-local-delete" 30 | state: absent 31 | 32 | - name: Create a minimal config remote repository with user/pass 33 | artifactory_repo: 34 | artifactory_url: "{{ artifactory_url }}" 35 | username: "{{ artifactory_user }}" 36 | password: "{{ artifactory_password }}" 37 | repo: "test-remote-creation" 38 | state: present 39 | repo_config: '{"rclass": "remote", "url": "http://http://host:port/some-repo"}' 40 | 41 | - name: Create a minimal config remote repository with auth_token 42 | artifactory_repo: 43 | artifactory_url: "{{ artifactory_url }}" 44 | auth_token: "{{ artifactory_auth_token }}" 45 | repo: "test-virtual-creation" 46 | state: present 47 | repo_config: '{"rclass": "virtual", "packageType": "generic"}' 48 | 49 | - name: Update a virtual repository in artifactory with user/pass 50 | artifactory_repo: 51 | artifactory_url: "{{ artifactory_url }}" 52 | username: "{{ artifactory_user }}" 53 | password: "{{ artifactory_password }}" 54 | repo: "test-virtual-update" 55 | state: present 56 | repo_config: '{"description": "New public description."}' 57 | 58 | - name: Update a virtual repository and register current config after update. 59 | artifactory_repo: 60 | artifactory_url: "{{ artifactory_url }}" 61 | auth_token: "{{ artifactory_auth_token }}" 62 | repo: "test-virtual-update" 63 | state: present 64 | repo_config: '{"description": "New public description."}' 65 | register: test_virtual_config 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Artifactory 2 | =========== 3 | 4 | A collection (hopefully) of modules for JFrog Artifactory to improve management of the system. Plans to submit to ansible proper. The role does not directly perform any actions. 5 | 6 | Example Playbook 7 | ---------------- 8 | 9 | Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: 10 | 11 | - hosts: servers 12 | roles: 13 | - { role: quadewarren.artifactory } 14 | tasks: 15 | - name: create test-local-creation repo 16 | artifactory_repo: 17 | artifactory_url: https://artifactory.repo.example.com 18 | auth_token: my_token 19 | repo: "test-local-creation" 20 | state: present 21 | repo_config: '{"rclass": "local"}' 22 | 23 | - name: Delete a local repository in artifactory with auth_token 24 | artifactory_repo: 25 | artifactory_url: https://artifactory.repo.example.com 26 | auth_token: your_token 27 | repo: "test-local-delete" 28 | state: absent 29 | 30 | - name: Create a minimal config remote repository with user/pass 31 | artifactory_repo: 32 | artifactory_url: https://artifactory.repo.example.com 33 | username: your_username 34 | password: your_pass 35 | repo: "test-remote-creation" 36 | state: present 37 | repo_config: '{"rclass": "remote", "url": "http://http://host:port/some-repo"}' 38 | 39 | - name: Create a minimal config remote repository with auth_token 40 | artifactory_repo: 41 | artifactory_url: https://artifactory.repo.example.com 42 | auth_token: your_token 43 | repo: "test-virtual-creation" 44 | state: present 45 | repo_config: '{"rclass": "virtual", "packageType": "generic"}' 46 | 47 | - name: Update a virtual repository in artifactory with user/pass 48 | artifactory_repo: 49 | artifactory_url: https://artifactory.repo.example.com 50 | username: your_username 51 | password: your_pass 52 | repo: "test-virtual-update" 53 | state: present 54 | repo_config: '{"description": "New public description."}' 55 | 56 | - name: Update a virtual repository and register current config after update. 57 | artifactory_repo: 58 | artifactory_url: https://artifactory.repo.example.com 59 | auth_token: your_token 60 | repo: "test-virtual-update" 61 | state: present 62 | repo_config: '{"description": "New public description."}' 63 | register: test_virtual_config 64 | 65 | License 66 | ------- 67 | 68 | GPLV3+ (submitting to Ansible proper, and this is how other modules are licensed) 69 | 70 | Author Information 71 | ------------------ 72 | 73 | Written by Kyle Haley (quadewarren) 74 | Rolified by Greg Swift 75 | -------------------------------------------------------------------------------- /library/artifactory_groups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | from __future__ import absolute_import, division, print_function 16 | __metaclass__ = type 17 | 18 | 19 | ANSIBLE_METADATA = { 20 | 'metadata_version': '1.1', 21 | 'status': ['preview'], 22 | 'supported_by': 'community' 23 | } 24 | 25 | 26 | DOCUMENTATION = ''' 27 | --- 28 | module: artifactory_groups 29 | 30 | short_description: Provides management operations for security operations in JFrog Artifactory 31 | 32 | version_added: "2.5" 33 | 34 | description: 35 | - Provides basic management operations against security groups in JFrog 36 | Artifactory 5+. Please reference this configuration spec for the creation 37 | of users, groups, or permission targets. 38 | U(https://www.jfrog.com/confluence/display/RTF/Security+Configuration+JSON) 39 | 40 | options: 41 | artifactory_url: 42 | description: 43 | - The target URL for managing artifactory. For certain operations, 44 | you can include the group name appended to the end of the 45 | url. 46 | required: true 47 | name: 48 | description: 49 | - Name of the artifactory security group target to perform 50 | CRUD operations against. WARNING: The UI will enforce lowercase 51 | when importing LDAP groups, but the API side WILL not. If you are 52 | creating LDAP groups via the API, you will need to make sure all 53 | LDAP group names are lowercase since this will impact how 54 | Artifactory matches. See Artifactory Knowledge Article: 000001563 55 | required: true 56 | group_config: 57 | description: 58 | - The string representations of the JSON used to create the target 59 | security group. Please reference the JFrog Artifactory Security 60 | Configuration JSON for directions on what key/values to use. 61 | username: 62 | description: 63 | - username to be used in Basic Auth against Artifactory. Not 64 | required if using auth_token for basic auth. 65 | auth_password: 66 | description: 67 | - password to be used in Basic Auth against Artifactory. Not 68 | required if using auth_token for basic auth. 69 | auth_token: 70 | description: 71 | - authentication token to be used in Basic Auth against 72 | Artifactory. Not required if using username/auth_password for 73 | basic auth. 74 | validate_certs: 75 | description: 76 | - True to validate SSL certificates, False otherwise. 77 | type: bool 78 | default: false 79 | client_cert: 80 | description: 81 | - PEM formatted certificate chain file to be used for SSL client 82 | authentication. This file can also include the key as well, and 83 | if the key is included, I(client_key) is not required 84 | client_key: 85 | description: 86 | - PEM formatted file that contains your private key to be used for 87 | SSL client authentication. If I(client_cert) contains both the 88 | certificate and key, this option is not required. 89 | force_basic_auth: 90 | description: 91 | - The library used by the uri module only sends authentication 92 | information when a webservice responds to an initial request 93 | with a 401 status. Since some basic auth services do not properly 94 | send a 401, logins will fail. This option forces the sending of 95 | the Basic authentication header upon initial request. 96 | type: bool 97 | default: false 98 | state: 99 | description: 100 | - The state the artifactory security group should be in. 101 | 'present' ensures that the target exists, but is not replaced. 102 | The configuration supplied will overwrite the configuration that 103 | exists. 'absent' ensures that the the target is deleted. 104 | 'read' will return the configuration if the target exists. 105 | 'list' will return a list of all targets against the specified 106 | url that currently exist in the target artifactory. If you wish 107 | to, for instance, append a list of repositories that a permission 108 | target has access to, you will need to construct the complete 109 | list outside of the module and pass it in. 110 | choices: 111 | - present 112 | - absent 113 | - read 114 | - list 115 | default: read 116 | group_config_dict: 117 | description: 118 | - A dictionary in yaml format of valid configuration values against 119 | an artifactory security group. These 120 | dictionary values must match any other values passed in, such as 121 | those within top-level parameters or within the configuration 122 | string in group_config. 123 | 124 | author: 125 | - Kyle Haley (@quadewarren) 126 | ''' 127 | 128 | EXAMPLES = ''' 129 | # Create a security group using top-level parameters 130 | - name: create a new group using config hash 131 | artifactory_groups: 132 | artifactory_url: http://art.url.com/artifactory/api/security/groups 133 | auth_token: MY_TOKEN 134 | name: "temp-group" 135 | group_config_dict: 136 | description: A group representing a collection of users. Can be LDAP. 137 | state: present 138 | 139 | - name: update the group using config hash 140 | artifactory_groups: 141 | artifactory_url: http://art.url.com/artifactory/api/security/groups 142 | auth_token: MY_TOKEN 143 | name: "temp-group" 144 | group_config_dict: 145 | description: A group of users from LDAP. Can be LDAP. 146 | realm: "Realm name (e.g. ARTIFACTORY, CROWD)" 147 | state: present 148 | 149 | - name: delete the security group 150 | artifactory_groups: 151 | artifactory_url: http://art.url.com/artifactory/api/security/groups 152 | auth_token: MY_TOKEN 153 | name: "temp-group" 154 | state: absent 155 | ''' 156 | 157 | RETURN = ''' 158 | original_message: 159 | description: 160 | - A brief sentence describing what action the module was attempting 161 | to take against which artifactory security group and what 162 | artifactory url. 163 | returned: success 164 | type: str 165 | message: 166 | description: The result of the attempted action. 167 | returned: success 168 | type: str 169 | config: 170 | description: 171 | - The configuration of a successfully created security group, 172 | an updated security group (whether or not changed=True), or 173 | the config of a security group that was successfully deleted. 174 | returned: success 175 | type: dict 176 | ''' 177 | 178 | 179 | import ast 180 | 181 | import ansible.module_utils.artifactory as art_base 182 | 183 | from ansible.module_utils.basic import AnsibleModule 184 | 185 | 186 | URI_CONFIG_MAP = {"api/security/groups": True} 187 | 188 | 189 | def main(): 190 | state_map = ['present', 'absent', 'read', 'list'] 191 | module_args = dict( 192 | artifactory_url=dict(type='str', required=True), 193 | name=dict(type='str', default=''), 194 | group_config=dict(type='str', default=None), 195 | username=dict(type='str', default=None), 196 | auth_password=dict(type='str', no_log=True, default=None), 197 | auth_token=dict(type='str', no_log=True, default=None), 198 | validate_certs=dict(type='bool', default=False), 199 | client_cert=dict(type='path', default=None), 200 | client_key=dict(type='path', default=None), 201 | force_basic_auth=dict(type='bool', default=False), 202 | state=dict(type='str', default='read', choices=state_map), 203 | group_config_dict=dict(type='dict', default=dict()), 204 | ) 205 | 206 | result = dict( 207 | changed=False, 208 | original_message='', 209 | message='', 210 | config=dict() 211 | ) 212 | 213 | module = AnsibleModule( 214 | argument_spec=module_args, 215 | required_together=[['username', 'auth_password']], 216 | required_one_of=[['auth_password', 'auth_token']], 217 | mutually_exclusive=[['auth_password', 'auth_token']], 218 | required_if=[['state', 'present', ['artifactory_url', 'name']], 219 | ['state', 'absent', ['artifactory_url', 'name']], 220 | ['state', 'read', ['artifactory_url', 'name']]], 221 | supports_check_mode=True, 222 | ) 223 | 224 | artifactory_url = module.params['artifactory_url'] 225 | name = module.params['name'] 226 | group_config = module.params['group_config'] 227 | username = module.params['username'] 228 | auth_password = module.params['auth_password'] 229 | auth_token = module.params['auth_token'] 230 | validate_certs = module.params['validate_certs'] 231 | client_cert = module.params['client_cert'] 232 | client_key = module.params['client_key'] 233 | force_basic_auth = module.params['force_basic_auth'] 234 | state = module.params['state'] 235 | group_config_dict = module.params['group_config_dict'] 236 | 237 | if group_config: 238 | # temporarily convert to dict for validation 239 | group_config = ast.literal_eval(group_config) 240 | 241 | fail_messages = [] 242 | 243 | fails = art_base.validate_config_params(group_config, group_config_dict, 244 | 'group_config', 245 | 'group_config_dict') 246 | fail_messages.extend(fails) 247 | fails = art_base.validate_top_level_params('name', module, group_config, 248 | group_config_dict, 249 | 'group_config', 250 | 'group_config_dict') 251 | fail_messages.extend(fails) 252 | 253 | # Populate failure messages 254 | failure_message = "".join(fail_messages) 255 | 256 | # Conflicting config values should not be resolved 257 | if failure_message: 258 | module.fail_json(msg=failure_message, **result) 259 | 260 | sec_dict = dict() 261 | if module.params['name']: 262 | sec_dict['name'] = module.params['name'] 263 | if group_config: 264 | sec_dict.update(group_config) 265 | if group_config_dict: 266 | sec_dict.update(group_config_dict) 267 | # Artifactory stores the group name as lowercase (even if it was passed as 268 | # multi-case). Calls against that group after it is created will fail 269 | # since artifactory only recognizes the lower case name. 270 | sec_dict['name'] = sec_dict['name'].lower() 271 | name = name.lower() 272 | group_config = str(sec_dict) 273 | 274 | result['original_message'] = ("Perform state '%s' against target '%s' " 275 | "within artifactory '%s'" 276 | % (state, name, artifactory_url)) 277 | 278 | art_grp = art_base.ArtifactoryBase( 279 | artifactory_url=artifactory_url, 280 | name=name, 281 | art_config=group_config, 282 | username=username, 283 | password=auth_password, 284 | auth_token=auth_token, 285 | validate_certs=validate_certs, 286 | client_cert=client_cert, 287 | client_key=client_key, 288 | force_basic_auth=force_basic_auth, 289 | config_map=URI_CONFIG_MAP) 290 | art_base.run_module(module, art_grp, "groups", result, 291 | fail_messages, group_config) 292 | 293 | 294 | if __name__ == "__main__": 295 | main() 296 | -------------------------------------------------------------------------------- /library/artifactory_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | from __future__ import absolute_import, division, print_function 16 | __metaclass__ = type 17 | 18 | 19 | ANSIBLE_METADATA = { 20 | 'metadata_version': '1.1', 21 | 'status': ['preview'], 22 | 'supported_by': 'community' 23 | } 24 | 25 | 26 | DOCUMENTATION = ''' 27 | --- 28 | module: artifactory_security 29 | 30 | short_description: Provides management operations for security operations in JFrog Artifactory 31 | 32 | version_added: "2.5" 33 | 34 | description: 35 | - Provides basic management operations against security operations in JFrog 36 | Artifactory 5+. Please reference this configuration spec for the creation 37 | of users, groups, or permission targets. 38 | U(https://www.jfrog.com/confluence/display/RTF/Security+Configuration+JSON) 39 | 40 | options: 41 | artifactory_url: 42 | description: 43 | - The target URL for managing artifactory. For certain operations, 44 | you can include the group name appended to the end of the 45 | url. 46 | required: true 47 | name: 48 | description: 49 | - Name of the user target to perform 50 | CRUD operations against. 51 | required: true 52 | user_config: 53 | description: 54 | - The string representations of the JSON used to create the target 55 | security user. Please reference the JFrog Artifactory Security 56 | Configuration JSON for directions on what key/values to use. 57 | username: 58 | description: 59 | - username to be used in Basic Auth against Artifactory. Not 60 | required if using auth_token for basic auth. 61 | auth_password: 62 | description: 63 | - password to be used in Basic Auth against Artifactory. Not 64 | required if using auth_token for basic auth. 65 | auth_token: 66 | description: 67 | - authentication token to be used in Basic Auth against 68 | Artifactory. Not required if using username/auth_password for 69 | basic auth. 70 | validate_certs: 71 | description: 72 | - True to validate SSL certificates, False otherwise. 73 | type: bool 74 | default: false 75 | client_cert: 76 | description: 77 | - PEM formatted certificate chain file to be used for SSL client 78 | authentication. This file can also include the key as well, and 79 | if the key is included, I(client_key) is not required 80 | client_key: 81 | description: 82 | - PEM formatted file that contains your private key to be used for 83 | SSL client authentication. If I(client_cert) contains both the 84 | certificate and key, this option is not required. 85 | force_basic_auth: 86 | description: 87 | - The library used by the uri module only sends authentication 88 | information when a webservice responds to an initial request 89 | with a 401 status. Since some basic auth services do not properly 90 | send a 401, logins will fail. This option forces the sending of 91 | the Basic authentication header upon initial request. 92 | type: bool 93 | default: false 94 | state: 95 | description: 96 | - The state the target user should be in. 97 | 'present' ensures that the target exists, but is not replaced. 98 | The configuration supplied will overwrite the configuration that 99 | exists. 'absent' ensures that the the target is deleted. 100 | 'read' will return the configuration if the target exists. 101 | 'list' will return a list of all targets against the specified 102 | url that currently exist in the target artifactory. If you wish 103 | to, for instance, append a list of repositories that a permission 104 | target has access to, you will need to construct the complete 105 | list outside of the module and pass it in. 106 | choices: 107 | - present 108 | - absent 109 | - read 110 | - list 111 | default: read 112 | user_config_dict: 113 | description: 114 | - A dictionary in yaml format of valid configuration values against 115 | a user. These dictionary values must match any other values 116 | passed in, such as those within top-level parameters or within 117 | the configuration string in user_config. 118 | password: 119 | description: 120 | - The password used for creating a new user within Artifactory. It 121 | will not be displayed in the log output. 122 | email: 123 | description: 124 | - The email used for creating a new user within Artifactory. 125 | 126 | author: 127 | - Kyle Haley (@quadewarren) 128 | ''' 129 | 130 | EXAMPLES = ''' 131 | # Create a user using top-level parameters 132 | - name: create a temp user 133 | artifactory_users: 134 | artifactory_url: http://artifactory.url.com/artifactory/api/security/users 135 | auth_token: MY_TOKEN 136 | name: temp-user 137 | email: whatever@email.com 138 | password: whatever 139 | state: present 140 | 141 | # Update the user config using top-level parameters 142 | - name: update a user config using top-level parameters 143 | artifactory_users: 144 | artifactory_url: http://artifactory.url.com/artifactory/api/security/users 145 | auth_token: MY_TOKEN 146 | name: temp-user 147 | email: whatever@diffemail.com 148 | state: present 149 | 150 | - name: delete the temp user 151 | artifactory_users: 152 | artifactory_url: http://artifactory.url.com/artifactory/api/security/users 153 | auth_token: MY_TOKEN 154 | name: temp-user 155 | state: absent 156 | ''' 157 | 158 | RETURN = ''' 159 | original_message: 160 | description: 161 | - A brief sentence describing what action the module was attempting 162 | to take against the user configuration and what artifactory url. 163 | returned: success 164 | type: str 165 | message: 166 | description: The result of the attempted action. 167 | returned: success 168 | type: str 169 | config: 170 | description: 171 | - The configuration of a successfully created user, 172 | an updated user (whether or not changed=True), or 173 | the config of a user that was successfully deleted. 174 | returned: success 175 | type: dict 176 | ''' 177 | import ast 178 | 179 | import ansible.module_utils.artifactory as art_base 180 | 181 | from ansible.module_utils.basic import AnsibleModule 182 | 183 | 184 | USER_CONFIG_MAP = { 185 | "email": 186 | {"always_required": True}, 187 | "password": 188 | {"always_required": True}} 189 | URI_CONFIG_MAP = {"api/security/users": USER_CONFIG_MAP} 190 | 191 | 192 | def main(): 193 | state_map = ['present', 'absent', 'read', 'list'] 194 | module_args = dict( 195 | artifactory_url=dict(type='str', required=True), 196 | name=dict(type='str', default=''), 197 | user_config=dict(type='str', default=None), 198 | username=dict(type='str', default=None), 199 | auth_password=dict(type='str', no_log=True, default=None), 200 | auth_token=dict(type='str', no_log=True, default=None), 201 | validate_certs=dict(type='bool', default=False), 202 | client_cert=dict(type='path', default=None), 203 | client_key=dict(type='path', default=None), 204 | force_basic_auth=dict(type='bool', default=False), 205 | state=dict(type='str', default='read', choices=state_map), 206 | user_config_dict=dict(type='dict', default=dict()), 207 | password=dict(type='str', no_log=True, default=None), 208 | email=dict(type='str', default=None), 209 | ) 210 | 211 | result = dict( 212 | changed=False, 213 | original_message='', 214 | message='', 215 | config=dict() 216 | ) 217 | 218 | module = AnsibleModule( 219 | argument_spec=module_args, 220 | required_together=[['username', 'auth_password']], 221 | required_one_of=[['auth_password', 'auth_token']], 222 | mutually_exclusive=[['auth_password', 'auth_token']], 223 | required_if=[['state', 'present', ['artifactory_url', 'name']], 224 | ['state', 'absent', ['artifactory_url', 'name']], 225 | ['state', 'read', ['artifactory_url', 'name']]], 226 | supports_check_mode=True, 227 | ) 228 | 229 | artifactory_url = module.params['artifactory_url'] 230 | name = module.params['name'] 231 | user_config = module.params['user_config'] 232 | username = module.params['username'] 233 | auth_password = module.params['auth_password'] 234 | auth_token = module.params['auth_token'] 235 | validate_certs = module.params['validate_certs'] 236 | client_cert = module.params['client_cert'] 237 | client_key = module.params['client_key'] 238 | force_basic_auth = module.params['force_basic_auth'] 239 | state = module.params['state'] 240 | user_config_dict = module.params['user_config_dict'] 241 | 242 | if user_config: 243 | # temporarily convert to dict for validation 244 | user_config = ast.literal_eval(user_config) 245 | 246 | fail_messages = [] 247 | 248 | fails = art_base.validate_config_params(user_config, user_config_dict, 249 | 'user_config', 250 | 'user_config_dict') 251 | fail_messages.extend(fails) 252 | fails = art_base.validate_top_level_params('name', module, user_config, 253 | user_config_dict, 254 | 'user_config', 255 | 'user_config_dict') 256 | fail_messages.extend(fails) 257 | fails = art_base.validate_top_level_params('password', module, user_config, 258 | user_config_dict, 259 | 'user_config', 260 | 'user_config_dict') 261 | fail_messages.extend(fails) 262 | fails = art_base.validate_top_level_params('email', module, user_config, 263 | user_config_dict, 264 | 'user_config', 265 | 'user_config_dict') 266 | fail_messages.extend(fails) 267 | 268 | # Populate failure messages 269 | failure_message = "".join(fail_messages) 270 | 271 | # Conflicting config values should not be resolved 272 | if failure_message: 273 | module.fail_json(msg=failure_message, **result) 274 | 275 | sec_dict = dict() 276 | if module.params['name']: 277 | sec_dict['name'] = module.params['name'] 278 | if module.params['password']: 279 | sec_dict['password'] = module.params['password'] 280 | if module.params['email']: 281 | sec_dict['email'] = module.params['email'] 282 | if user_config: 283 | sec_dict.update(user_config) 284 | if user_config_dict: 285 | sec_dict.update(user_config_dict) 286 | user_config = str(sec_dict) 287 | 288 | result['original_message'] = ("Perform state '%s' against target '%s' " 289 | "within artifactory '%s'" 290 | % (state, name, artifactory_url)) 291 | 292 | art_sec = art_base.ArtifactoryBase( 293 | artifactory_url=artifactory_url, 294 | name=name, 295 | art_config=user_config, 296 | username=username, 297 | password=auth_password, 298 | auth_token=auth_token, 299 | validate_certs=validate_certs, 300 | client_cert=client_cert, 301 | client_key=client_key, 302 | force_basic_auth=force_basic_auth, 303 | config_map=URI_CONFIG_MAP) 304 | ignore_keys = [] 305 | art_base.run_module(module, art_sec, "users", result, 306 | fail_messages, user_config, ignore_keys=ignore_keys) 307 | 308 | 309 | if __name__ == "__main__": 310 | main() 311 | -------------------------------------------------------------------------------- /library/artifactory_permissions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | from __future__ import absolute_import, division, print_function 16 | __metaclass__ = type 17 | 18 | 19 | ANSIBLE_METADATA = { 20 | 'metadata_version': '1.1', 21 | 'status': ['preview'], 22 | 'supported_by': 'community' 23 | } 24 | 25 | 26 | DOCUMENTATION = ''' 27 | --- 28 | module: artifactory_permissions 29 | 30 | short_description: Provides management operations for security operations in JFrog Artifactory 31 | 32 | version_added: "2.5" 33 | 34 | description: 35 | - Provides basic management operations against security operations in JFrog 36 | Artifactory 5+. Please reference this configuration spec for the creation 37 | of users, groups, or permission targets. 38 | U(https://www.jfrog.com/confluence/display/RTF/Security+Configuration+JSON) 39 | 40 | options: 41 | artifactory_url: 42 | description: 43 | - The target URL for managing artifactory. For certain operations, 44 | you can include the permission target name appended to the end 45 | of the url. 46 | required: true 47 | name: 48 | description: 49 | - Name of the artifactory permission target to perform 50 | CRUD operations against. 51 | required: true 52 | perm_config: 53 | description: 54 | - The string representations of the JSON used to create the 55 | artifactory permission target. Please reference the JFrog 56 | Artifactory Security Configuration JSON for directions on what 57 | key/values to use. 58 | username: 59 | description: 60 | - username to be used in Basic Auth against Artifactory. Not 61 | required if using auth_token for basic auth. 62 | auth_password: 63 | description: 64 | - password to be used in Basic Auth against Artifactory. Not 65 | required if using auth_token for basic auth. 66 | auth_token: 67 | description: 68 | - authentication token to be used in Basic Auth against 69 | Artifactory. Not required if using username/auth_password for 70 | basic auth. 71 | validate_certs: 72 | description: 73 | - True to validate SSL certificates, False otherwise. 74 | type: bool 75 | default: false 76 | client_cert: 77 | description: 78 | - PEM formatted certificate chain file to be used for SSL client 79 | authentication. This file can also include the key as well, and 80 | if the key is included, I(client_key) is not required 81 | client_key: 82 | description: 83 | - PEM formatted file that contains your private key to be used for 84 | SSL client authentication. If I(client_cert) contains both the 85 | certificate and key, this option is not required. 86 | force_basic_auth: 87 | description: 88 | - The library used by the uri module only sends authentication 89 | information when a webservice responds to an initial request 90 | with a 401 status. Since some basic auth services do not properly 91 | send a 401, logins will fail. This option forces the sending of 92 | the Basic authentication header upon initial request. 93 | type: bool 94 | default: false 95 | state: 96 | description: 97 | - The state the permission target should be in. 98 | 'present' ensures that the target exists, but is not replaced. 99 | The configuration supplied will overwrite the configuration that 100 | exists. 'absent' ensures that the the target is deleted. 101 | 'read' will return the configuration if the target exists. 102 | 'list' will return a list of all targets against the specified 103 | url that currently exist in the target artifactory. If you wish 104 | to, for instance, append a list of repositories that a permission 105 | target has access to, you will need to construct the complete 106 | list outside of the module and pass it in. 107 | choices: 108 | - present 109 | - absent 110 | - read 111 | - list 112 | default: read 113 | perm_config_dict: 114 | description: 115 | - A dictionary in yaml format of valid configuration values against 116 | a permission target. These dictionary values must match any 117 | other values passed in, such as those within top-level 118 | parameters or within the configuration 119 | string in perm_config. 120 | repositories: 121 | description: 122 | - The list of repositories associated with a permission target, 123 | which represents the list of repositories that permission target, 124 | can access. 125 | 126 | author: 127 | - Kyle Haley (@quadewarren) 128 | ''' 129 | 130 | EXAMPLES = ''' 131 | # Create a permission target using top-level parameters 132 | - name: create a permission target 133 | artifactory_permissions: 134 | artifactory_url: http://art.url.com/artifactory/api/security/permissions 135 | auth_token: MY_TOKEN 136 | name: temp-perm 137 | repositories: 138 | - one-repo 139 | - two-repo 140 | state: present 141 | 142 | # Create a permission target using top-level parameters and using 143 | # perm_config_dict to set group permissions. 144 | - name: create a permission target 145 | artifactory_permissions: 146 | artifactory_url: http://art.url.com/artifactory/api/security/permissions 147 | auth_token: MY_TOKEN 148 | name: temp-perm 149 | repositories: 150 | - one-repo 151 | - two-repo 152 | perm_config_dict: 153 | principals: 154 | groups: '{"the_group": ["w", "r"]}' 155 | state: present 156 | 157 | # Replace the artifactory permission target with the latest version 158 | - name: Replace the permission target with latest config 159 | artifactory_permissions: 160 | artifactory_url: http://art.url.com/artifactory/api/security/permissions 161 | auth_token: MY_TOKEN 162 | name: temp-perm 163 | repositories: 164 | - one-repo 165 | - two-repo 166 | - three-repo 167 | state: present 168 | 169 | - name: delete the permission target 170 | artifactory_permissions: 171 | artifactory_url: http://art.url.com/artifactory/api/security/permissions 172 | auth_token: MY_TOKEN 173 | name: temp-perm 174 | state: absent 175 | ''' 176 | 177 | RETURN = ''' 178 | original_message: 179 | description: 180 | - A brief sentence describing what action the module was attempting 181 | to take against the permission target and what artifactory url. 182 | returned: success 183 | type: str 184 | message: 185 | description: The result of the attempted action. 186 | returned: success 187 | type: str 188 | config: 189 | description: 190 | - The configuration of a successfully created permission target, 191 | an updated permission target (whether or not changed=True), or 192 | the config of a permission target that was successfully deleted. 193 | returned: success 194 | type: dict 195 | ''' 196 | import ast 197 | 198 | import ansible.module_utils.artifactory as art_base 199 | 200 | from ansible.module_utils.basic import AnsibleModule 201 | 202 | 203 | PERMISSION_CONFIG_MAP = { 204 | "repositories": 205 | {"always_required": True}} 206 | URI_CONFIG_MAP = {"api/security/permissions": PERMISSION_CONFIG_MAP} 207 | 208 | 209 | class ArtifactoryPermissions(art_base.ArtifactoryBase): 210 | def __init__(self, artifactory_url, name=None, 211 | perm_config=None, username=None, password=None, 212 | auth_token=None, validate_certs=False, client_cert=None, 213 | client_key=None, force_basic_auth=False, config_map=None): 214 | super(ArtifactoryPermissions, self).__init__( 215 | username=username, 216 | password=password, 217 | auth_token=auth_token, 218 | validate_certs=validate_certs, 219 | client_cert=client_cert, 220 | client_key=client_key, 221 | force_basic_auth=force_basic_auth, 222 | config_map=config_map, 223 | artifactory_url=artifactory_url, 224 | name=name, 225 | art_config=perm_config) 226 | 227 | def update_artifactory_target(self): 228 | return self.create_artifactory_target() 229 | 230 | 231 | def main(): 232 | state_map = ['present', 'absent', 'read', 'list'] 233 | module_args = dict( 234 | artifactory_url=dict(type='str', required=True), 235 | name=dict(type='str', default=''), 236 | perm_config=dict(type='str', default=None), 237 | username=dict(type='str', default=None), 238 | auth_password=dict(type='str', no_log=True, default=None), 239 | auth_token=dict(type='str', no_log=True, default=None), 240 | validate_certs=dict(type='bool', default=False), 241 | client_cert=dict(type='path', default=None), 242 | client_key=dict(type='path', default=None), 243 | force_basic_auth=dict(type='bool', default=False), 244 | state=dict(type='str', default='read', choices=state_map), 245 | perm_config_dict=dict(type='dict', default=dict()), 246 | repositories=dict(type='list', default=None), 247 | ) 248 | 249 | result = dict( 250 | changed=False, 251 | original_message='', 252 | message='', 253 | config=dict() 254 | ) 255 | 256 | module = AnsibleModule( 257 | argument_spec=module_args, 258 | required_together=[['username', 'auth_password']], 259 | required_one_of=[['auth_password', 'auth_token']], 260 | mutually_exclusive=[['auth_password', 'auth_token']], 261 | required_if=[['state', 'present', ['artifactory_url', 'name']], 262 | ['state', 'absent', ['artifactory_url', 'name']], 263 | ['state', 'read', ['artifactory_url', 'name']]], 264 | supports_check_mode=True, 265 | ) 266 | 267 | artifactory_url = module.params['artifactory_url'] 268 | name = module.params['name'] 269 | perm_config = module.params['perm_config'] 270 | username = module.params['username'] 271 | auth_password = module.params['auth_password'] 272 | auth_token = module.params['auth_token'] 273 | validate_certs = module.params['validate_certs'] 274 | client_cert = module.params['client_cert'] 275 | client_key = module.params['client_key'] 276 | force_basic_auth = module.params['force_basic_auth'] 277 | state = module.params['state'] 278 | perm_config_dict = module.params['perm_config_dict'] 279 | 280 | if perm_config: 281 | # temporarily convert to dict for validation 282 | perm_config = ast.literal_eval(perm_config) 283 | 284 | fail_messages = [] 285 | 286 | fails = art_base.validate_config_params(perm_config, perm_config_dict, 287 | 'perm_config', 288 | 'perm_config_dict') 289 | fail_messages.extend(fails) 290 | fails = art_base.validate_top_level_params('name', module, perm_config, 291 | perm_config_dict, 292 | 'perm_config', 293 | 'perm_config_dict') 294 | fail_messages.extend(fails) 295 | fails = art_base.validate_top_level_params('repositories', module, 296 | perm_config, 297 | perm_config_dict, 298 | 'perm_config', 299 | 'perm_config_dict') 300 | fail_messages.extend(fails) 301 | 302 | # Populate failure messages 303 | failure_message = "".join(fail_messages) 304 | 305 | # Conflicting config values should not be resolved 306 | if failure_message: 307 | module.fail_json(msg=failure_message, **result) 308 | 309 | sec_dict = dict() 310 | if module.params['name']: 311 | sec_dict['name'] = module.params['name'] 312 | if module.params['repositories']: 313 | sec_dict['repositories'] = module.params['repositories'] 314 | if perm_config: 315 | sec_dict.update(perm_config) 316 | if perm_config_dict: 317 | sec_dict.update(perm_config_dict) 318 | # Artifactory stores the name as lowercase (even if it was passed as 319 | # multi-case). Calls against that name after it is created will fail 320 | # since artifactory only recognizes the lower case name. 321 | sec_dict['name'] = sec_dict['name'].lower() 322 | name = name.lower() 323 | perm_config = str(sec_dict) 324 | 325 | result['original_message'] = ("Perform state '%s' against target '%s' " 326 | "within artifactory '%s'" 327 | % (state, name, artifactory_url)) 328 | 329 | art_perm = ArtifactoryPermissions( 330 | artifactory_url=artifactory_url, 331 | name=name, 332 | perm_config=perm_config, 333 | username=username, 334 | password=auth_password, 335 | auth_token=auth_token, 336 | validate_certs=validate_certs, 337 | client_cert=client_cert, 338 | client_key=client_key, 339 | force_basic_auth=force_basic_auth, 340 | config_map=URI_CONFIG_MAP) 341 | art_base.run_module(module, art_perm, "permission target", result, 342 | fail_messages, perm_config) 343 | 344 | 345 | if __name__ == "__main__": 346 | main() 347 | -------------------------------------------------------------------------------- /library/artifactory_replication.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | from __future__ import absolute_import, division, print_function 16 | __metaclass__ = type 17 | 18 | 19 | ANSIBLE_METADATA = { 20 | 'metadata_version': '1.1', 21 | 'status': ['preview'], 22 | 'supported_by': 'community' 23 | } 24 | 25 | 26 | DOCUMENTATION = ''' 27 | --- 28 | module: artifactory_replication 29 | 30 | short_description: Provides management repositoriy replication in JFrog Artifactory 31 | 32 | version_added: "2.5" 33 | 34 | description: 35 | - Provides basic management operations for configuration repository 36 | replication in JFrog Artifactory. 37 | 38 | options: 39 | artifactory_url: 40 | description: 41 | - The target URL for managing artifactory. For certain operations, 42 | you can include the group name appended to the end of the 43 | url. 44 | required: true 45 | name: 46 | description: 47 | - Name of the local repository to configure replication against. 48 | required: true 49 | replication_config: 50 | description: 51 | - The string representations of the JSON used to configure the 52 | replication for the given repository. 53 | username: 54 | description: 55 | - username to be used in Basic Auth against Artifactory. Not 56 | required if using auth_token for basic auth. 57 | auth_password: 58 | description: 59 | - password to be used in Basic Auth against Artifactory. Not 60 | required if using auth_token for basic auth. 61 | auth_token: 62 | description: 63 | - authentication token to be used in Basic Auth against 64 | Artifactory. Not required if using username/auth_password for 65 | basic auth. 66 | validate_certs: 67 | description: 68 | - True to validate SSL certificates, False otherwise. 69 | type: bool 70 | default: false 71 | client_cert: 72 | description: 73 | - PEM formatted certificate chain file to be used for SSL client 74 | authentication. This file can also include the key as well, and 75 | if the key is included, I(client_key) is not required 76 | client_key: 77 | description: 78 | - PEM formatted file that contains your private key to be used for 79 | SSL client authentication. If I(client_cert) contains both the 80 | certificate and key, this option is not required. 81 | force_basic_auth: 82 | description: 83 | - The library used by the uri module only sends authentication 84 | information when a webservice responds to an initial request 85 | with a 401 status. Since some basic auth services do not properly 86 | send a 401, logins will fail. This option forces the sending of 87 | the Basic authentication header upon initial request. 88 | type: bool 89 | default: false 90 | state: 91 | description: 92 | - The state the replication configuration should be in. 93 | 'present' ensures that the target exists, but is not replaced. 94 | The configuration supplied will overwrite the configuration that 95 | exists. 'absent' ensures that the the target is deleted. 96 | 'read' will return the configuration if the target exists. 97 | choices: 98 | - present 99 | - absent 100 | - read 101 | default: read 102 | replication_config_dict: 103 | description: 104 | - A dictionary in yaml format of valid configuration for repository 105 | replication. These dictionary values must match any other values 106 | passed in, such as those within top-level parameters or within 107 | the configuration string in replication_config. 108 | cronExp: 109 | description: 110 | - A cron expression that represents the schedule that push/pull 111 | replication will take place. This is independent of event based 112 | replication. 113 | enableEventReplication: 114 | description: 115 | - Enable event based replication whenever a repository is created, 116 | updated, or deleted.. 117 | type: bool 118 | default: false 119 | remote_url: 120 | description: 121 | - The url of the target repository to configure push or pull 122 | replication against. 123 | replication_username: 124 | description: 125 | - The username Artifactory will use to execute push/pull 126 | replication operations against a remote. 127 | replication_password: 128 | description: 129 | - The password Artifactory will use to execute push/pull 130 | replication operations against a remote. 131 | 132 | 133 | author: 134 | - Kyle Haley (@quadewarren) 135 | ''' 136 | 137 | EXAMPLES = ''' 138 | # Configure replication on an existing repo with cron and event based 139 | # replication configured. 140 | - name: Configure replication for a repository 141 | artifactory_replication: 142 | artifactory_url: http://art.url.com/artifactory/api/replications/ 143 | auth_token: MY_TOKEN 144 | name: bower-local 145 | cronExp: "0 0 12 * * ?" 146 | enableEventReplication: true 147 | replication_username: remote_username 148 | replication_password: my_password 149 | remote_url: http://the.remote.repo.com/artifactory/remote-repo 150 | state: present 151 | 152 | # Update the replication configuration, set event based replication to false 153 | - name: Update the replication configuration for a repository 154 | artifactory_replication: 155 | artifactory_url: http://art.url.com/artifactory/api/replications/ 156 | auth_token: MY_TOKEN 157 | name: bower-local 158 | enableEventReplication: false 159 | state: present 160 | 161 | - name: delete the replication configuration 162 | artifactory_replication: 163 | artifactory_url: http://art.url.com/artifactory/api/replications/ 164 | auth_token: MY_TOKEN 165 | name: bower-local 166 | state: absent 167 | ''' 168 | 169 | RETURN = ''' 170 | original_message: 171 | description: 172 | - A brief sentence describing what action the module was attempting 173 | to make against the replication configuration and what 174 | artifactory url. 175 | returned: success 176 | type: str 177 | message: 178 | description: The result of the attempted action. 179 | returned: success 180 | type: str 181 | config: 182 | description: 183 | - The configuration of a successfully created replication config, 184 | an updated replication config (whether or not changed=True), or 185 | the config of a replication config that was successfully deleted. 186 | returned: success 187 | type: dict 188 | ''' 189 | import ast 190 | 191 | import ansible.module_utils.artifactory as art_base 192 | 193 | from ansible.module_utils.basic import AnsibleModule 194 | 195 | 196 | REPLICATION_CONFIG_MAP = { 197 | "name": 198 | {"always_required": True}, 199 | "remote_url": 200 | {"always_required": True}} 201 | URI_CONFIG_MAP = {"api/replications": REPLICATION_CONFIG_MAP} 202 | 203 | 204 | def main(): 205 | state_map = ['present', 'absent', 'read', 'list'] 206 | module_args = dict( 207 | artifactory_url=dict(type='str', required=True), 208 | name=dict(type='str', required=True), 209 | replication_config=dict(type='str', default=None), 210 | username=dict(type='str', default=None), 211 | auth_password=dict(type='str', no_log=True, default=None), 212 | auth_token=dict(type='str', no_log=True, default=None), 213 | validate_certs=dict(type='bool', default=False), 214 | client_cert=dict(type='path', default=None), 215 | client_key=dict(type='path', default=None), 216 | force_basic_auth=dict(type='bool', default=False), 217 | state=dict(type='str', default='read', choices=state_map), 218 | replication_config_dict=dict(type='dict', default=dict()), 219 | remote_url=dict(type='str', default=None), 220 | replication_username=dict(type='str', default=None), 221 | replication_password=dict(type='str', default=None), 222 | cronExp=dict(type='str', default=None), 223 | enableEventReplication=dict(type='str', default=None), 224 | ) 225 | 226 | result = dict( 227 | changed=False, 228 | original_message='', 229 | message='', 230 | config=dict() 231 | ) 232 | 233 | module = AnsibleModule( 234 | argument_spec=module_args, 235 | required_together=[['username', 'auth_password']], 236 | required_one_of=[['auth_password', 'auth_token']], 237 | mutually_exclusive=[['auth_password', 'auth_token']], 238 | required_if=[['state', 'present', 239 | ['artifactory_url', 'name', 240 | 'remote_url', 'replication_username', 241 | 'replication_password']], 242 | ['state', 'absent', ['artifactory_url', 'name']], 243 | ['state', 'read', ['artifactory_url', 'name']]], 244 | supports_check_mode=True, 245 | ) 246 | 247 | artifactory_url = module.params['artifactory_url'] 248 | name = module.params['name'] 249 | replication_config = module.params['replication_config'] 250 | username = module.params['username'] 251 | auth_password = module.params['auth_password'] 252 | auth_token = module.params['auth_token'] 253 | validate_certs = module.params['validate_certs'] 254 | client_cert = module.params['client_cert'] 255 | client_key = module.params['client_key'] 256 | force_basic_auth = module.params['force_basic_auth'] 257 | state = module.params['state'] 258 | replication_config_dict = module.params['replication_config_dict'] 259 | 260 | if replication_config: 261 | # temporarily convert to dict for validation 262 | replication_config = ast.literal_eval(replication_config) 263 | 264 | fail_messages = [] 265 | 266 | fails = art_base.validate_config_params(replication_config, 267 | replication_config_dict, 268 | 'replication_config', 269 | 'replication_config_dict') 270 | fail_messages.extend(fails) 271 | fails = art_base.validate_top_level_params('name', module, 272 | replication_config, 273 | replication_config_dict, 274 | 'replication_config', 275 | 'replication_config_dict') 276 | fail_messages.extend(fails) 277 | fails = art_base.validate_top_level_params('remote_url', module, 278 | replication_config, 279 | replication_config_dict, 280 | 'replication_config', 281 | 'replication_config_dict') 282 | fail_messages.extend(fails) 283 | fails = art_base.validate_top_level_params('replication_username', module, 284 | replication_config, 285 | replication_config_dict, 286 | 'replication_config', 287 | 'replication_config_dict') 288 | fail_messages.extend(fails) 289 | fails = art_base.validate_top_level_params('replication_password', module, 290 | replication_config, 291 | replication_config_dict, 292 | 'replication_config', 293 | 'replication_config_dict') 294 | fail_messages.extend(fails) 295 | fails = art_base.validate_top_level_params('cronExp', module, 296 | replication_config, 297 | replication_config_dict, 298 | 'replication_config', 299 | 'replication_config_dict') 300 | fail_messages.extend(fails) 301 | fails = art_base.validate_top_level_params('enableEventReplication', 302 | module, 303 | replication_config, 304 | replication_config_dict, 305 | 'replication_config', 306 | 'replication_config_dict') 307 | fail_messages.extend(fails) 308 | 309 | # Populate failure messages 310 | failure_message = "".join(fail_messages) 311 | 312 | # Conflicting config values should not be resolved 313 | if failure_message: 314 | module.fail_json(msg=failure_message, **result) 315 | 316 | sec_dict = dict() 317 | if module.params['name']: 318 | sec_dict['name'] = module.params['name'] 319 | if module.params['remote_url']: 320 | sec_dict['remote_url'] = module.params['remote_url'] 321 | if module.params['email']: 322 | sec_dict['email'] = module.params['email'] 323 | if module.params['replication_username']: 324 | sec_dict['replication_username'] =\ 325 | module.params['replication_username'] 326 | if module.params['replication_password']: 327 | sec_dict['replication_password'] =\ 328 | module.params['replication_password'] 329 | if module.params['cronExp']: 330 | sec_dict['cronExp'] = module.params['cronExp'] 331 | if module.params['enableEventReplication']: 332 | sec_dict['enableEventReplication'] =\ 333 | module.params['enableEventReplication'] 334 | if replication_config: 335 | sec_dict.update(replication_config) 336 | if replication_config_dict: 337 | sec_dict.update(replication_config_dict) 338 | replication_config = str(sec_dict) 339 | 340 | result['original_message'] = ("Perform state '%s' against target '%s' " 341 | "within artifactory '%s'" 342 | % (state, name, artifactory_url)) 343 | 344 | art_replication = art_base.ArtifactoryBase( 345 | artifactory_url=artifactory_url, 346 | name=name, 347 | art_config=replication_config, 348 | username=username, 349 | password=auth_password, 350 | auth_token=auth_token, 351 | validate_certs=validate_certs, 352 | client_cert=client_cert, 353 | client_key=client_key, 354 | force_basic_auth=force_basic_auth, 355 | config_map=URI_CONFIG_MAP) 356 | art_base.run_module(module, art_replication, "Repository replication", 357 | result, fail_messages, replication_config) 358 | 359 | 360 | if __name__ == "__main__": 361 | main() 362 | -------------------------------------------------------------------------------- /library/artifactory_repo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | from __future__ import absolute_import, division, print_function 16 | __metaclass__ = type 17 | 18 | 19 | ANSIBLE_METADATA = { 20 | 'metadata_version': '1.1', 21 | 'status': ['preview'], 22 | 'supported_by': 'community' 23 | } 24 | 25 | 26 | DOCUMENTATION = ''' 27 | --- 28 | module: artifactory_repo 29 | 30 | short_description: Provides management operations for repositories in JFrog Artifactory 31 | 32 | version_added: "2.5" 33 | 34 | description: 35 | - "Provides basic management operations against repositories JFrog Artifactory 5+." 36 | 37 | options: 38 | artifactory_url: 39 | description: 40 | - The target URL for managing artifactory. For certain operations, 41 | you can include the repository key appended to the end of the 42 | url. 43 | required: true 44 | name: 45 | description: 46 | - Name of the target repo to perform CRUD operations against. 47 | required: true 48 | repo_position: 49 | description: 50 | - Sets the resolution order for which repos of the same type are 51 | queried. This governs the order local, remote repositories 52 | and other virtual repositories are listed in the virtual 53 | repository configuration. By default, this is ignored so that 54 | the order is governed by the order of creation. 55 | required: false 56 | repo_config: 57 | description: 58 | - The configuration for the given repository as a json formatted 59 | string. This configuration must conform to JFrog Artifactory 60 | Repository Configuration JSON published at jfrog.com . Basic 61 | validation is performed against required fields for Artifactory 62 | 5+ for required fields in create/replace calls, which is the 63 | following... 'rclass' must be defined for all create calls. If 64 | creating a 'virtual' repository, 'packageType' 65 | must be defined with an appropriate type. If creating 66 | a 'remote' repository, 'url' must be defined in the 67 | configuration. 68 | required: false 69 | username: 70 | description: 71 | - username to be used in Basic Auth against Artifactory. Not 72 | required if using auth_token for basic auth. 73 | required: false 74 | password: 75 | description: 76 | - password to be used in Basic Auth against Artifactory. Not 77 | required if using auth_token for basic auth. 78 | required: false 79 | auth_token: 80 | description: 81 | - authentication token to be used in Basic Auth against 82 | Artifactory. Not required if using username/password for basic 83 | auth. 84 | required: false 85 | validate_certs: 86 | description: 87 | - True to validate SSL certificates, False otherwise. 88 | required: false 89 | choices: 90 | - True 91 | - False 92 | default: False 93 | client_cert: 94 | description: 95 | - PEM formatted certificate chain file to be used for SSL client 96 | authentication. This file can also include the key as well, and 97 | if the key is included, I(client_key) is not required 98 | required: false 99 | client_key: 100 | description: 101 | - PEM formatted file that contains your private key to be used for 102 | SSL client authentication. If I(client_cert) contains both the 103 | certificate and key, this option is not required. 104 | required: false 105 | force_basic_auth: 106 | description: 107 | - The library used by the uri module only sends authentication 108 | information when a webservice responds to an initial request 109 | with a 401 status. Since some basic auth services do not properly 110 | send a 401, logins will fail. This option forces the sending of 111 | the Basic authentication header upon initial request. 112 | required: false 113 | choices: 114 | - True 115 | - False 116 | default: False 117 | state: 118 | description: 119 | - The state the repository should be in. 'present' ensures that a 120 | repository exists, but not replaced. 121 | 'absent' ensures that the repository is deleted. 122 | 'read' will return the configuration if the repository exists. 123 | 'list' will return a list of all repositories that currently 124 | exist in the target artifactory. The configuration supplied 125 | will overwrite the configuration that exists. If you wish to, for 126 | instance, append new repositories to a virtual repository, you 127 | will need to construct the complete list outside of the module 128 | and pass it in. 129 | required: false 130 | choices: 131 | - present 132 | - absent 133 | - read 134 | - list 135 | default: read 136 | rclass: 137 | description: 138 | - The type of remote repository you wish to create. 139 | required: false 140 | choices: 141 | - local 142 | - remote 143 | - virtual 144 | url: 145 | description: 146 | - The url for the repository. 147 | required: false 148 | packageType: 149 | description: 150 | - The packageType of the repository. 151 | required: false 152 | choices: 153 | - alpine 154 | - bower 155 | - chef 156 | - cocoapods 157 | - composer 158 | - conan 159 | - conda 160 | - cran 161 | - debian 162 | - docker 163 | - gems 164 | - generic 165 | - gitlfs 166 | - go 167 | - gradle 168 | - helm 169 | - ivy 170 | - maven 171 | - npm 172 | - nuget 173 | - opkg 174 | - p2 175 | - puppet 176 | - pypi 177 | - rpm 178 | - sbt 179 | - vagrant 180 | - vcs 181 | repo_config_dict: 182 | description: 183 | - A dictionary in yaml format of valid configuration values. These 184 | dictionary key/value pairs match the configuration spec of 185 | repo_config. 186 | required: false 187 | repoLayoutRef: 188 | description: 189 | - The name of the target layout for this repository. 190 | required: false 191 | 192 | author: 193 | - Kyle Haley (@quadewarren) 194 | ''' 195 | 196 | EXAMPLES = ''' 197 | # Create a local repository in artifactory with auth_token with minimal 198 | # config requirements 199 | - name: create test-local-creation repo 200 | artifactory_repo: 201 | artifactory_url: https://artifactory.repo.example.com 202 | auth_token: my_token 203 | name: test-local-creation 204 | state: present 205 | repo_config: '{"rclass": "local"}' 206 | 207 | # Create a local repository in artifactory with auth_token using top level params 208 | # config requirements 209 | - name: create test-local-creation repo 210 | artifactory_repo: 211 | artifactory_url: https://artifactory.repo.example.com 212 | auth_token: my_token 213 | name: test-local-creation 214 | state: present 215 | rclass: local 216 | 217 | # Create a local repository in artifactory with multiple levels of params 218 | # config requirements 219 | - name: create test-local-creation repo 220 | artifactory_repo: 221 | artifactory_url: https://artifactory.repo.example.com 222 | auth_token: my_token 223 | name: test-local-creation 224 | state: present 225 | rclass: local 226 | repo_config_dict: 227 | packageType: generic 228 | repo_config: '{"description": "Test description"}' 229 | 230 | # Delete a local repository in artifactory with auth_token 231 | - name: delete test-local-delete repo 232 | artifactory_repo: 233 | artifactory_url: https://artifactory.repo.example.com 234 | auth_token: your_token 235 | name: test-local-delete 236 | state: absent 237 | 238 | # Create a remote repository in artifactory with username/password with 239 | # minimal config requirements 240 | - name: create test-remote-creation repo 241 | artifactory_repo: 242 | artifactory_url: https://artifactory.repo.example.com 243 | username: your_username 244 | password: your_pass 245 | name: test-remote-creation 246 | state: present 247 | repo_config: '{"rclass": "remote", "url": "http://http://host:port/some-repo"}' 248 | 249 | # Create a virtual repository in artifactory with auth_token with 250 | # minimal config requirements 251 | - name: create test-remote-creation repo 252 | artifactory_repo: 253 | artifactory_url: https://artifactory.repo.example.com 254 | auth_token: your_token 255 | name: test-virtual-creation 256 | state: present 257 | repo_config: '{"rclass": "virtual", "packageType": "generic"}' 258 | 259 | # Update a virtual repository in artifactory with username/password 260 | - name: update test-virtual-update repo 261 | artifactory_repo: 262 | artifactory_url: https://artifactory.repo.example.com 263 | username: your_username 264 | password: your_pass 265 | name: test-virtual-update 266 | state: present 267 | repo_config: '{"description": "New public description."}' 268 | register: test_virtual_config 269 | 270 | # Repository config is in response for successful create/update calls, 271 | # regardless if call resulted in a change. Successful delete calls 272 | # contain the config of the repo just before deletion for later use in play. 273 | - name: dump test_virtual_config config json 274 | debug: 275 | msg: '{{ test_virtual_config.config }}' 276 | 277 | # Update a virtual repository with a config hash 278 | # Configuration provided replaces the existing configuration, so 279 | # if you want to append values to an existing list, that list needs to be 280 | # constructed outside of the module and passed in. 281 | - name: update test-virtual-update repo 282 | artifactory_repo: 283 | artifactory_url: https://artifactory.repo.example.com 284 | auth_token: your_token 285 | name: test-virtual-update 286 | state: present 287 | repo_config_dict: 288 | description: "Another new public description" 289 | repositories: 290 | - pypi-remote 291 | - mypi-local 292 | ''' 293 | 294 | RETURN = ''' 295 | original_message: 296 | description: 297 | - A brief sentence describing what action the module was attempting 298 | to take against which repository and what artifactory url. 299 | returned: success 300 | type: str 301 | message: 302 | description: The result of the attempted action. 303 | returned: success 304 | type: str 305 | config: 306 | description: 307 | - The configuration of a successfully created repository, an updated 308 | repository (whether or not changed=True), or the config 309 | of a repository that was successfully deleted. 310 | returned: success 311 | type: dict 312 | ''' 313 | 314 | 315 | import ast 316 | 317 | import ansible.module_utils.artifactory as art_base 318 | 319 | from ansible.module_utils.basic import AnsibleModule 320 | 321 | 322 | LOCAL_RCLASS = "local" 323 | REMOTE_RCLASS = "remote" 324 | VIRTUAL_RCLASS = "virtual" 325 | 326 | VALID_RCLASSES = [LOCAL_RCLASS, REMOTE_RCLASS, VIRTUAL_RCLASS] 327 | 328 | VALID_PACKAGETYPES = ["alpine", 329 | "bower", 330 | "chef", 331 | "cocoapods", 332 | "composer", 333 | "conan", 334 | "conda", 335 | "cran", 336 | "debian", 337 | "docker", 338 | "gems", 339 | "generic", 340 | "gitlfs", 341 | "go", 342 | "gradle", 343 | "helm", 344 | "ivy", 345 | "maven", 346 | "npm", 347 | "nuget", 348 | "opkg", 349 | "p2", 350 | "puppet", 351 | "pypi", 352 | "rpm", 353 | "sbt", 354 | "vagrant", 355 | "vcs" 356 | ] 357 | 358 | KEY_CONFIG_MAP = { 359 | "rclass": 360 | {"valid_values": VALID_RCLASSES, 361 | "values_require_keys": 362 | {VIRTUAL_RCLASS: ["packageType"], 363 | REMOTE_RCLASS: ["url"]}, 364 | "always_required": True}, 365 | "packageType": 366 | {"valid_values": VALID_PACKAGETYPES}} 367 | 368 | URI_CONFIG_MAP = {"api/repositories": KEY_CONFIG_MAP} 369 | 370 | 371 | class ArtifactoryRepoManagement(art_base.ArtifactoryBase): 372 | def __init__(self, artifactory_url, repo=None, repo_position=None, 373 | repo_config=None, username=None, password=None, 374 | auth_token=None, validate_certs=False, client_cert=None, 375 | client_key=None, force_basic_auth=False, config_map=None): 376 | super(ArtifactoryRepoManagement, self).__init__( 377 | username=username, 378 | password=password, 379 | auth_token=auth_token, 380 | validate_certs=validate_certs, 381 | client_cert=client_cert, 382 | client_key=client_key, 383 | force_basic_auth=force_basic_auth, 384 | config_map=config_map, 385 | artifactory_url=artifactory_url, 386 | name=repo, 387 | art_config=repo_config) 388 | self.repo_position = repo_position 389 | 390 | def create_artifactory_target(self): 391 | method = 'PUT' 392 | serial_config_data = self.get_valid_conf(method) 393 | create_repo_url = self.working_url 394 | if self.repo_position: 395 | if isinstance(self.repo_position, int): 396 | create_repo_url = '%s?pos=%d' % (create_repo_url, 397 | self.repo_position) 398 | else: 399 | raise ValueError("repo_position must be an integer.") 400 | 401 | return self.query_artifactory(create_repo_url, method, 402 | data=serial_config_data) 403 | 404 | 405 | def main(): 406 | state_map = ['present', 'absent', 'read', 'list'] 407 | rclass_state_map = VALID_RCLASSES 408 | packageType_state_map = VALID_PACKAGETYPES 409 | module_args = dict( 410 | artifactory_url=dict(type='str', required=True), 411 | name=dict(type='str', required=True), 412 | repo_position=dict(type='int', default=None), 413 | repo_config=dict(type='str', default=None), 414 | username=dict(type='str', default=None), 415 | password=dict(type='str', no_log=True, default=None), 416 | auth_token=dict(type='str', no_log=True, default=None), 417 | validate_certs=dict(type='bool', default=False), 418 | client_cert=dict(type='path', default=None), 419 | client_key=dict(type='path', default=None), 420 | force_basic_auth=dict(type='bool', default=False), 421 | state=dict(type='str', default='read', choices=state_map), 422 | rclass=dict(type='str', default=None, choices=rclass_state_map), 423 | packageType=dict(type='str', default=None, 424 | choices=packageType_state_map), 425 | url=dict(type='str', default=None), 426 | repoLayoutRef=dict(type='str', default=None), 427 | repo_config_dict=dict(type='dict', default=dict()), 428 | ) 429 | 430 | result = dict( 431 | changed=False, 432 | original_message='', 433 | message='', 434 | config=dict() 435 | ) 436 | 437 | module = AnsibleModule( 438 | argument_spec=module_args, 439 | required_together=[['username', 'password']], 440 | required_one_of=[['password', 'auth_token']], 441 | mutually_exclusive=[['password', 'auth_token']], 442 | required_if=[['state', 'present', ['artifactory_url', 'name']], 443 | ['state', 'absent', ['artifactory_url', 'name']], 444 | ['state', 'read', ['artifactory_url', 'name']]], 445 | supports_check_mode=True, 446 | ) 447 | 448 | artifactory_url = module.params['artifactory_url'] 449 | repository = module.params['name'] 450 | repo_position = module.params['repo_position'] 451 | repo_config = module.params['repo_config'] 452 | username = module.params['username'] 453 | password = module.params['password'] 454 | auth_token = module.params['auth_token'] 455 | validate_certs = module.params['validate_certs'] 456 | client_cert = module.params['client_cert'] 457 | client_key = module.params['client_key'] 458 | force_basic_auth = module.params['force_basic_auth'] 459 | state = module.params['state'] 460 | repo_config_dict = module.params['repo_config_dict'] 461 | 462 | if repo_config: 463 | # temporarily convert to dict for validation 464 | repo_config = ast.literal_eval(repo_config) 465 | 466 | fail_messages = [] 467 | 468 | fails = art_base.validate_config_params(repo_config, repo_config_dict, 469 | 'repo_config', 'repo_config_dict') 470 | fail_messages.extend(fails) 471 | 472 | fails = art_base.validate_top_level_params('rclass', module, repo_config, 473 | repo_config_dict, 'repo_config', 474 | 'repo_config_dict') 475 | fail_messages.extend(fails) 476 | fails = art_base.validate_top_level_params('packageType', module, 477 | repo_config, repo_config_dict, 478 | 'repo_config', 479 | 'repo_config_dict') 480 | fail_messages.extend(fails) 481 | fails = art_base.validate_top_level_params('url', module, repo_config, 482 | repo_config_dict, 'repo_config', 483 | 'repo_config_dict') 484 | fail_messages.extend(fails) 485 | fails = art_base.validate_top_level_params('repoLayoutRef', 486 | module, repo_config, 487 | repo_config_dict, 'repo_config', 488 | 'repo_config_dict') 489 | fail_messages.extend(fails) 490 | 491 | # Populate failure messages 492 | failure_message = "".join(fail_messages) 493 | 494 | # Conflicting config values should not be resolved 495 | if failure_message: 496 | module.fail_json(msg=failure_message, **result) 497 | 498 | repo_dict = dict() 499 | if module.params['name']: 500 | repo_dict['key'] = module.params['name'] 501 | if module.params['rclass']: 502 | repo_dict['rclass'] = module.params['rclass'] 503 | if module.params['packageType']: 504 | repo_dict['packageType'] = module.params['packageType'] 505 | if module.params['url']: 506 | repo_dict['url'] = module.params['url'] 507 | if module.params['repoLayoutRef']: 508 | repo_dict['repoLayoutRef'] = module.params['repoLayoutRef'] 509 | if repo_config: 510 | repo_dict.update(repo_config) 511 | if repo_config_dict: 512 | repo_dict.update(repo_config_dict) 513 | repo_config = str(repo_dict) 514 | 515 | result['original_message'] = ("Perform state '%s' against repo '%s' " 516 | "within artifactory '%s'" 517 | % (state, repository, artifactory_url)) 518 | 519 | art_repo = ArtifactoryRepoManagement( 520 | artifactory_url=artifactory_url, 521 | repo=repository, 522 | repo_position=repo_position, 523 | repo_config=repo_config, 524 | username=username, 525 | password=password, 526 | auth_token=auth_token, 527 | validate_certs=validate_certs, 528 | client_cert=client_cert, 529 | client_key=client_key, 530 | force_basic_auth=force_basic_auth, 531 | config_map=URI_CONFIG_MAP) 532 | 533 | art_base.run_module(module, art_repo, "repos", result, 534 | fail_messages, repo_config) 535 | 536 | 537 | if __name__ == "__main__": 538 | main() 539 | -------------------------------------------------------------------------------- /lib/ansible/module_utils/artifactory.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 Kyle Haley, 2 | 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | 6 | # 1. Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | 9 | # 2. Redistributions in binary form must reproduce the above copyright notice, 10 | # this list of conditions and the following disclaimer in the documentation 11 | # and/or other materials provided with the distribution. 12 | 13 | # 3. Neither the name of the copyright holder nor the names of its 14 | # contributors may be used to endorse or promote products derived from 15 | # this software without specific prior written permission. 16 | 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | # POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import ansible.module_utils.six.moves.urllib.error as urllib_error 30 | import ast 31 | import json 32 | 33 | from ansible.module_utils.six import iteritems 34 | from ansible.module_utils.six.moves.urllib.parse import quote 35 | from ansible.module_utils.urls import open_url 36 | 37 | """ 38 | This is the general construction of the validation map passed in. 39 | URI_CONFIG_MAP["uri_key"] is a substring of the target artifactory URL, 40 | such as "api/repositories" or "api/security/groups". The need for URL substring 41 | exists since some modules may need to cover multiple URLs, such as a security 42 | module, which may need to touch on 43 | "api/security/groups" or "api/security/users" or "api/security/permissions". 44 | 45 | KEY_CONFIG_MAP = { 46 | "config_key": 47 | {"valid_values": list(), 48 | "required_keys": list(), 49 | "values_require_keys": dict(), 50 | "always_required": bool}} 51 | 52 | URI_CONFIG_MAP = { 53 | "uri_key": KEY_CONFIG_MAP} 54 | """ 55 | 56 | 57 | class ArtifactoryBase(object): 58 | def __init__(self, username=None, password=None, artifactory_url=None, 59 | auth_token=None, validate_certs=False, client_cert=None, 60 | client_key=None, force_basic_auth=False, config_map=None, 61 | name=None, art_config=None): 62 | self.username = username 63 | self.password = password 64 | self.auth_token = auth_token 65 | 66 | self.validate_certs = validate_certs 67 | self.client_cert = client_cert 68 | self.client_key = client_key 69 | self.force_basic_auth = force_basic_auth 70 | 71 | self.artifactory_url = artifactory_url 72 | self.name = name 73 | self.config_map = config_map 74 | self.art_config = art_config 75 | 76 | self.headers = {"Content-Type": "application/json"} 77 | if auth_token: 78 | self.headers["X-JFrog-Art-Api"] = auth_token 79 | 80 | if self.name: 81 | # escape invalid url characters 82 | self.working_url = '%s/%s' % (self.artifactory_url, 83 | quote(self.name)) 84 | else: 85 | self.working_url = self.artifactory_url 86 | 87 | def get_artifactory_targets(self): 88 | return self.query_artifactory(self.artifactory_url, 'GET') 89 | 90 | def get_artifactory_target(self): 91 | return self.query_artifactory(self.working_url, 'GET') 92 | 93 | def delete_artifactory_target(self): 94 | return self.query_artifactory(self.working_url, 'DELETE') 95 | 96 | def create_artifactory_target(self): 97 | # This is not a mistake. POST == PUT in artifactory land 98 | method = 'PUT' 99 | serial_config_data = self.get_valid_conf(method) 100 | create_target_url = self.working_url 101 | return self.query_artifactory(create_target_url, method, 102 | data=serial_config_data) 103 | 104 | def update_artifactory_target(self): 105 | # This is not a mistake. PUT == POST in artifactory land 106 | method = 'POST' 107 | serial_config_data = self.get_valid_conf(method) 108 | return self.query_artifactory(self.working_url, method, 109 | data=serial_config_data) 110 | 111 | def get_valid_conf(self, method): 112 | config_dict = self.convert_config_to_dict(self.art_config) 113 | if method == 'PUT': 114 | self.validate_config_required_keys(self.artifactory_url, 115 | config_dict) 116 | self.validate_config_values(self.artifactory_url, config_dict) 117 | serial_config_data = self.serialize_config_data(config_dict) 118 | return serial_config_data 119 | 120 | def convert_config_to_dict(self, config): 121 | if isinstance(config, dict): 122 | return config 123 | else: 124 | error_occurred = False 125 | message = "" 126 | try: 127 | test_dict = ast.literal_eval(config) 128 | if isinstance(test_dict, dict): 129 | config = test_dict 130 | else: 131 | raise ValueError() 132 | except ValueError as ve: 133 | error_occurred = True 134 | message = str(ve) 135 | except SyntaxError as se: 136 | error_occurred = True 137 | message = str(se) 138 | 139 | if error_occurred: 140 | raise ConfigValueTypeMismatch("Configuration data provided " 141 | "is not valid json.\n %s" 142 | % message) 143 | return config 144 | 145 | def serialize_config_data(self, config_data): 146 | if not config_data or not isinstance(config_data, dict): 147 | raise InvalidConfigurationData("Config is null, empty, or is not" 148 | " a dictionary.") 149 | serial_config_data = json.dumps(config_data) 150 | return serial_config_data 151 | 152 | def query_artifactory(self, url, method, data=None): 153 | if self.auth_token: 154 | response = open_url(url, data=data, headers=self.headers, 155 | method=method, 156 | validate_certs=self.validate_certs, 157 | client_cert=self.client_cert, 158 | client_key=self.client_key, 159 | force_basic_auth=self.force_basic_auth) 160 | else: 161 | response = open_url(url, data=data, headers=self.headers, 162 | method=method, 163 | validate_certs=self.validate_certs, 164 | client_cert=self.client_cert, 165 | client_key=self.client_key, 166 | force_basic_auth=self.force_basic_auth, 167 | url_username=self.username, 168 | url_password=self.password) 169 | return response 170 | 171 | def validate_config_values(self, url, config_dict): 172 | validation_dict = self.get_uri_key_map(url, self.config_map) 173 | if not validation_dict or isinstance(validation_dict, bool): 174 | return 175 | for config_key in config_dict: 176 | if config_key in validation_dict: 177 | if "valid_values" in validation_dict[config_key]: 178 | valid_values = validation_dict[config_key]["valid_values"] 179 | config_val = config_dict[config_key] 180 | if valid_values and config_val not in valid_values: 181 | except_message = ("'%s' is not a valid value for " 182 | "key '%s'" 183 | % (config_val, config_key)) 184 | raise InvalidConfigurationData(except_message) 185 | 186 | def get_uri_key_map(self, url, uri_config_map): 187 | if not url or not uri_config_map: 188 | raise InvalidConfigurationData("url or config is None or empty." 189 | " url: %s, config: %s" 190 | % (url, uri_config_map)) 191 | temp = None 192 | for uri_substr in uri_config_map: 193 | if uri_substr in url: 194 | temp = uri_config_map[uri_substr] 195 | break 196 | if temp: 197 | return temp 198 | else: 199 | raise InvalidArtifactoryURL("The url '%s' could not be " 200 | "mapped to a known set of " 201 | "configuration rules." % url) 202 | 203 | def validate_config_required_keys(self, url, config_dict): 204 | req_keys = self.get_always_required_keys(url, config_dict) 205 | for required in req_keys: 206 | if required not in config_dict: 207 | message = ("%s key is missing from config." % required) 208 | raise InvalidConfigurationData(message) 209 | return req_keys 210 | 211 | def get_always_required_keys(self, url, config_dict): 212 | """Return keys that are always required for creating a target.""" 213 | validation_dict = self.get_uri_key_map(url, self.config_map) 214 | req_keys = list() 215 | # If the resulting validation dict is boolean True, then this just 216 | # verifies that the url is correct for this module. Return empty list. 217 | if not validation_dict or isinstance(validation_dict, bool): 218 | return req_keys 219 | for config_req in validation_dict: 220 | if "always_required" in validation_dict[config_req]: 221 | if validation_dict[config_req]["always_required"]: 222 | req_keys.append(config_req) 223 | for config_key in config_dict: 224 | if config_key in validation_dict: 225 | valid_sub_dict = validation_dict[config_key] 226 | # If config_key exists, check if other keys are required. 227 | if "required_keys" in valid_sub_dict: 228 | if isinstance(valid_sub_dict["required_keys"], list): 229 | req_keys.extend(valid_sub_dict["required_keys"]) 230 | else: 231 | raise InvalidConfigurationData( 232 | "Values defined in 'required_keys' should be" 233 | " a list. ['%s']['required_keys'] is not a" 234 | " list." % config_key) 235 | # If config_key exists, check if the value of config_key 236 | # requires other keys. 237 | # If the value of the key 'rclass' is 'remote', then the 'url' 238 | # key must be defined. The value in the mapping should be a 239 | # list. 240 | if "values_require_keys" in valid_sub_dict: 241 | config_value = config_dict[config_key] 242 | val_req_keys = valid_sub_dict["values_require_keys"] 243 | if val_req_keys and config_value in val_req_keys: 244 | if isinstance(val_req_keys[config_value], list): 245 | req_keys.extend(val_req_keys[config_value]) 246 | else: 247 | raise InvalidConfigurationData( 248 | "Values defined in in the dict" 249 | " 'values_require_keys' should be lists." 250 | " ['values_require_keys']['%s'] is not a" 251 | " list." % config_value) 252 | return req_keys 253 | 254 | def compare_config(self, current, desired, ignore_keys=list()): 255 | def order_dict_sort_list(dictionary): 256 | result = {} 257 | for k, v in sorted(dictionary.items()): 258 | if isinstance(v, dict): 259 | result[k] = order_dict_sort_list(v) 260 | elif isinstance(v, list): 261 | result[k] = sorted(v) 262 | elif isinstance(v, str): 263 | if v.isdigit(): 264 | result[k] = int(v) 265 | else: 266 | result[k] = v 267 | else: 268 | result[k] = v 269 | return result 270 | 271 | s_current = order_dict_sort_list(current) 272 | s_desired = order_dict_sort_list(desired) 273 | return all(s_current[k] == s_desired[k] 274 | for k in s_desired if k in s_current and 275 | k not in ignore_keys) 276 | 277 | 278 | class InvalidArtifactoryURL(Exception): 279 | pass 280 | 281 | 282 | class ConfigValueTypeMismatch(Exception): 283 | pass 284 | 285 | 286 | class InvalidConfigurationData(Exception): 287 | pass 288 | 289 | 290 | TOP_LEVEL_FAIL = ("Conflicting config values. " 291 | "top level parameter {1} != {0}[{1}]. " 292 | "Only one config value need be set. ") 293 | 294 | 295 | def validate_top_level_params(top_level_param, module, config, config_hash, 296 | config_name, config_hash_name): 297 | """Validate top-level params against different configuration sources. 298 | These modules can have multiple configuration sources. If these sources 299 | have identical keys, but different values, aggregate error messages to 300 | alert the user for each one that does not match and the source. 301 | return a list of those messages. 302 | """ 303 | validation_fail_messages = [] 304 | config_hash_fail_msg = "" 305 | config_fail_msg = "" 306 | if not top_level_param or not module.params[top_level_param]: 307 | return validation_fail_messages 308 | value = module.params[top_level_param] 309 | if isinstance(value, list): 310 | value = sorted(value) 311 | if config_hash and top_level_param in config_hash: 312 | if isinstance(config_hash[top_level_param], list): 313 | config_hash[top_level_param] = sorted(config_hash[top_level_param]) 314 | if value != config_hash[top_level_param]: 315 | config_hash_fail_msg = TOP_LEVEL_FAIL.format(config_hash_name, 316 | top_level_param) 317 | validation_fail_messages.append(config_hash_fail_msg) 318 | if config and top_level_param in config: 319 | if isinstance(config[top_level_param], list): 320 | config[top_level_param] = sorted(config[top_level_param]) 321 | if value != config[top_level_param]: 322 | config_fail_msg = TOP_LEVEL_FAIL.format(config_name, 323 | top_level_param) 324 | validation_fail_messages.append(config_fail_msg) 325 | 326 | return validation_fail_messages 327 | 328 | 329 | CONFIG_PARAM_FAIL = ("Conflicting config values. " 330 | "{1}[{0}] != " 331 | "{2}[{0}]. " 332 | "Only one config value need be " 333 | "set. ") 334 | 335 | 336 | def validate_config_params(config, config_hash, config_name, config_hash_name): 337 | """Validate two different configuration sources. 338 | These modules can have multiple configuration sources. If these sources 339 | have identical keys, but different values, aggregate error messages to 340 | alert the user for each one that does not match and the source. 341 | return a list of those messages. 342 | """ 343 | validation_fail_messages = [] 344 | if not config_hash or not config: 345 | return validation_fail_messages 346 | for key, val in iteritems(config): 347 | if key in config_hash: 348 | if isinstance(config_hash[key], list): 349 | config_hash[key] = sorted(config_hash[key]) 350 | if isinstance(config[key], list): 351 | config[key] = sorted(config[key]) 352 | if config_hash[key] != config[key]: 353 | fail_msg = CONFIG_PARAM_FAIL.format(key, config_name, 354 | config_hash_name) 355 | validation_fail_messages.append(fail_msg) 356 | return validation_fail_messages 357 | 358 | 359 | def run_module(module, art_obj, message_noun, result, fail_messages, 360 | art_dict, ignore_keys=list()): 361 | state = module.params['state'] 362 | artifactory_url = module.params['artifactory_url'] 363 | target_name = module.params['name'] 364 | art_config_str = art_obj.art_config 365 | art_target_exists = False 366 | try: 367 | art_obj.get_artifactory_target() 368 | art_target_exists = True 369 | except urllib_error.HTTPError as http_e: 370 | if http_e.getcode() == 400 and 'api/repositories' in artifactory_url: 371 | # Instead of throwing a 404, a 400 is thrown if a repo doesn't 372 | # exist. Have to fall through and assume the repo doesn't exist 373 | # and that another error did not occur. If there is another problem 374 | # it will have to be caught by try/catch blocks further below. 375 | pass 376 | elif (http_e.getcode() == 404 and 377 | ('api/security/groups' in artifactory_url or 378 | 'api/security/users' in artifactory_url or 379 | 'api/security/permissions' in artifactory_url)): 380 | # If 404, the target is just not found. Continue on. 381 | pass 382 | else: 383 | message = ("HTTP response code was '%s'. Response message was" 384 | " '%s'. " % (http_e.getcode(), http_e.read())) 385 | fail_messages.append(message) 386 | except urllib_error.URLError as url_e: 387 | message = ("A generic URLError was thrown. URLError: %s" % str(url_e)) 388 | fail_messages.append(message) 389 | 390 | try: 391 | # Now that configs are lined up, verify required values in configs 392 | if state == 'present': 393 | art_obj.validate_config_values(artifactory_url, art_dict) 394 | if not art_target_exists: 395 | art_obj.validate_config_required_keys(artifactory_url, 396 | art_dict) 397 | except ConfigValueTypeMismatch as cvtm: 398 | fail_messages.append(cvtm.message + ". ") 399 | except InvalidConfigurationData as icd: 400 | fail_messages.append(icd.message + ". ") 401 | except InvalidArtifactoryURL as iau: 402 | fail_messages.append(iau.message + ". ") 403 | 404 | # Populate failure messages 405 | failure_message = "".join(fail_messages) 406 | 407 | if failure_message: 408 | module.fail_json(msg=failure_message, **result) 409 | 410 | if module.check_mode: 411 | result['message'] = 'check_mode success' 412 | module.exit_json(**result) 413 | 414 | art_targ_not_exists_msg = ("%s '%s' does not exist." 415 | % (message_noun, target_name)) 416 | resp_is_invalid_failure = ("An unknown error occurred while attempting to " 417 | "'%s' %s '%s'. Response should " 418 | "not be None.") 419 | resp = None 420 | try: 421 | if state == 'list': 422 | result['message'] = ("List of all artifactory targets against " 423 | "artifactory_url: %s" % artifactory_url) 424 | resp = art_obj.get_artifactory_targets() 425 | result['config'] = json.loads(resp.read()) 426 | elif state == 'read': 427 | if not art_target_exists: 428 | result['message'] = art_targ_not_exists_msg 429 | else: 430 | resp = art_obj.get_artifactory_target() 431 | if resp: 432 | result['message'] = ("Successfully read config " 433 | "on %s '%s'." 434 | % (message_noun, target_name)) 435 | result['config'] = json.loads(resp.read()) 436 | result['changed'] = True 437 | else: 438 | failure_message = (resp_is_invalid_failure 439 | % (state, message_noun, target_name)) 440 | elif state == 'present': 441 | # If the target doesn't exist, create it. 442 | # If the target does exist, perform an update on it ONLY if 443 | # configuration supplied has values that don't match the remote 444 | # config. 445 | if not art_target_exists: 446 | result['message'] = ('Attempting to create %s: %s' 447 | % (message_noun, target_name)) 448 | resp = art_obj.create_artifactory_target() 449 | if resp: 450 | result['message'] = resp.read() 451 | result['changed'] = True 452 | else: 453 | failure_message = (resp_is_invalid_failure 454 | % (state, message_noun, target_name)) 455 | else: 456 | result['message'] = ('Attempting to update %s: %s' 457 | % (message_noun, target_name)) 458 | current_config = art_obj.get_artifactory_target() 459 | current_config = json.loads(current_config.read()) 460 | desired_config = ast.literal_eval(art_config_str) 461 | # Compare desired config with current config against target. 462 | # If config values are identical, don't update. 463 | config_identical = art_obj.compare_config(current_config, 464 | desired_config) 465 | if not config_identical: 466 | resp = art_obj.update_artifactory_target() 467 | result['message'] = ("Successfully updated config " 468 | "on %s '%s'." 469 | % (message_noun, target_name)) 470 | result['changed'] = True 471 | else: 472 | # Config values were identical. 473 | result['message'] = ("%s '%s' was not updated because " 474 | "config was identical." 475 | % (message_noun, target_name)) 476 | # Attach the artfactory target config to result 477 | current_config = art_obj.get_artifactory_target() 478 | result['config'] = json.loads(current_config.read()) 479 | elif state == 'absent': 480 | if not art_target_exists: 481 | result['message'] = art_targ_not_exists_msg 482 | else: 483 | # save config for output on successful delete so it can be 484 | # used later in play if recreating targets 485 | current_config = art_obj.get_artifactory_target() 486 | resp = art_obj.delete_artifactory_target() 487 | if resp: 488 | result['message'] = ("Successfully deleted %s '%s'." 489 | % (message_noun, target_name)) 490 | result['changed'] = True 491 | result['config'] = json.loads(current_config.read()) 492 | else: 493 | failure_message = (resp_is_invalid_failure 494 | % (state, message_noun, target_name)) 495 | except urllib_error.HTTPError as http_e: 496 | message = ("HTTP response code was '%s'. Response message was" 497 | " '%s'. " % (http_e.getcode(), http_e.read())) 498 | failure_message = message 499 | except urllib_error.URLError as url_e: 500 | message = ("A generic URLError was thrown. URLError: %s" 501 | % str(url_e)) 502 | failure_message = message 503 | except SyntaxError as s_e: 504 | message = ("%s. Response from artifactory was malformed: '%s' . " 505 | % (str(s_e), resp)) 506 | failure_message = message 507 | except ValueError as v_e: 508 | message = ("%s. Response from artifactory was malformed: '%s' . " 509 | % (str(v_e), resp)) 510 | failure_message = message 511 | except ConfigValueTypeMismatch as cvtm: 512 | failure_message = cvtm.message 513 | except InvalidConfigurationData as icd: 514 | failure_message = icd.message 515 | 516 | if failure_message: 517 | module.fail_json(msg=failure_message, **result) 518 | 519 | module.exit_json(**result) 520 | --------------------------------------------------------------------------------