├── 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 |
--------------------------------------------------------------------------------