oo_azure_rm_publish_image.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. #!/usr/bin/env python
  2. # pylint: disable=missing-docstring
  3. # Copyright 2018 Red Hat, Inc. and/or its affiliates
  4. # and other contributors as indicated by the @author tags.
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License");
  7. # you may not use this file except in compliance with the License.
  8. # You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. #
  18. from __future__ import print_function # noqa: F401
  19. # import httplib
  20. import json
  21. import os
  22. import time
  23. import requests
  24. from ansible.module_utils.basic import AnsibleModule
  25. class AzurePublisherException(Exception):
  26. '''Exception class for AzurePublisher'''
  27. pass
  28. class AzurePublisher(object):
  29. '''Python class to represent the Azure Publishing portal https://cloudpartner.azure.com'''
  30. # pylint: disable=too-many-arguments
  31. def __init__(self,
  32. publisher_id,
  33. client_info,
  34. ssl_verify=True,
  35. api_version='2017-10-31',
  36. debug=False):
  37. '''
  38. :publisher_id: string of the publisher id
  39. :client_info: a dict containing the client_id, client_secret to get an access_token
  40. '''
  41. self._azure_server = 'https://cloudpartner.azure.com/api/publishers/{}'.format(publisher_id)
  42. self.client_info = client_info
  43. self.ssl_verify = ssl_verify
  44. self.api_version = 'api-version={}'.format(api_version)
  45. self.debug = debug
  46. # if self.debug:
  47. # import httplib
  48. # httplib.HTTPSConnection.debuglevel = 1
  49. # httplib.HTTPConnection.debuglevel = 1
  50. self._access_token = None
  51. @property
  52. def server(self):
  53. '''property for server url'''
  54. return self._azure_server
  55. @property
  56. def token(self):
  57. '''property for the access_token
  58. curl --data-urlencode "client_id=$AZURE_CLIENT_ID" \
  59. --data-urlencode "client_secret=$AZURE_CLIENT_SECRET" \
  60. --data-urlencode "grant_type=client_credentials" \
  61. --data-urlencode "resource=https://cloudpartner.azure.com" \
  62. https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/token
  63. '''
  64. if self._access_token is None:
  65. url = 'https://login.microsoftonline.com/{}/oauth2/token'.format(self.client_info['tenant_id'])
  66. data = {
  67. 'client_id': {self.client_info['client_id']},
  68. 'client_secret': self.client_info['client_secret'],
  69. 'grant_type': 'client_credentials',
  70. 'resource': 'https://cloudpartner.azure.com'
  71. }
  72. results = AzurePublisher.request('POST', url, data, {})
  73. jres = results.json()
  74. self._access_token = jres['access_token']
  75. return self._access_token
  76. def get_offers(self, offer=None, version=None, slot='preview'):
  77. ''' fetch all offers by publisherid '''
  78. url = '/offers'
  79. if offer is not None:
  80. url += '/{}'.format(offer)
  81. if version is not None:
  82. url += '/versions/{}'.format(version)
  83. if slot == 'preview':
  84. url += '/slot/{}'.format(slot)
  85. url += '?{}'.format(self.api_version)
  86. return self.prepare_action(url)
  87. def get_operations(self, offer, operation=None, status=None):
  88. ''' create or modify an offer '''
  89. url = '/offers/{0}/submissions'.format(offer)
  90. if operation is not None:
  91. url += '/operations/{0}'.format(operation)
  92. if not url.endswith('/'):
  93. url += '/'
  94. url += '?{0}'.format(self.api_version)
  95. if status is not None:
  96. url += '&status={0}'.format(status)
  97. return self.prepare_action(url, 'GET')
  98. def cancel_operation(self, offer):
  99. ''' create or modify an offer '''
  100. url = '/offers/{0}/cancel?{1}'.format(offer, self.api_version)
  101. return self.prepare_action(url, 'POST')
  102. def publish(self, offer, emails):
  103. ''' publish an offer '''
  104. url = '/offers/{0}/publish?{1}'.format(offer, self.api_version)
  105. data = {
  106. 'metadata': {
  107. 'notification-emails': ','.join(emails),
  108. }
  109. }
  110. return self.prepare_action(url, 'POST', data=data)
  111. def go_live(self, offer):
  112. ''' create or modify an offer '''
  113. url = '/offers/{0}/golive?{1}'.format(offer, self.api_version)
  114. return self.prepare_action(url, 'POST')
  115. def create_or_modify_offer(self, offer, data=None, modify=False):
  116. ''' create or modify an offer '''
  117. url = '/offers/{0}?{1}'.format(offer, self.api_version)
  118. headers = None
  119. if modify:
  120. headers = {
  121. 'If-Match': '*',
  122. }
  123. return self.prepare_action(url, 'PUT', data=data, add_headers=headers)
  124. def prepare_action(self, url, action='GET', data=None, add_headers=None):
  125. '''perform the http request
  126. :action: string of either GET|POST
  127. '''
  128. headers = {
  129. 'Content-Type': 'application/json',
  130. 'Accept': 'application/json',
  131. 'Authorization': 'Bearer {}'.format(self.token)
  132. }
  133. if add_headers is not None:
  134. headers.update(add_headers)
  135. if data is None:
  136. data = ''
  137. else:
  138. data = json.dumps(data)
  139. return AzurePublisher.request(action.upper(), self.server + url, data, headers)
  140. def cancel_and_wait_for_operation(self, params):
  141. '''cancel the current publish operation and wait for operation to complete'''
  142. # cancel the publish operation
  143. self.cancel_operation(offer=params['offer'])
  144. # we need to wait here for 'submissionState' to move to 'canceled'
  145. while True:
  146. # fetch operations
  147. ops = self.get_operations(params['offer'])
  148. if self.debug:
  149. print(ops.json())
  150. if ops.json()[0]['submissionState'] == 'canceled':
  151. break
  152. time.sleep(5)
  153. return ops
  154. def manage_offer(self, params):
  155. ''' handle creating or modifying offers'''
  156. # fetch the offer to verify it exists:
  157. results = self.get_offers(offer=params['offer'])
  158. if results.status_code == 200 and params['force']:
  159. return self.create_or_modify_offer(offer=params['offer'], data=params['offer_data'], modify=True)
  160. return self.create_or_modify_offer(offer=params['offer'], data=params['offer_data'])
  161. @staticmethod
  162. def request(action, url, data=None, headers=None, ssl_verify=True):
  163. req = requests.Request(action.upper(), url, data=data, headers=headers)
  164. session = requests.Session()
  165. req_prep = session.prepare_request(req)
  166. response = session.send(req_prep, verify=ssl_verify)
  167. return response
  168. @staticmethod
  169. def run_ansible(params):
  170. '''perform the ansible operations'''
  171. client_info = {
  172. 'tenant_id': params['tenant_id'],
  173. 'client_id': params['client_id'],
  174. 'client_secret': params['client_secret']}
  175. apc = AzurePublisher(params['publisher'],
  176. client_info,
  177. debug=params['debug'])
  178. if params['state'] == 'offer':
  179. results = apc.manage_offer(params)
  180. elif params['state'] == 'publish':
  181. results = apc.publish(offer=params['offer'], emails=params['emails'])
  182. results.json = lambda: ''
  183. elif params['state'] == 'cancel_op':
  184. results = apc.cancel_and_wait_for_operation(params)
  185. elif params['state'] == 'go_live':
  186. results = apc.go_live(offer=params['offer'])
  187. else:
  188. raise AzurePublisherException('Unsupported query type: {}'.format(params['state']))
  189. changed = False
  190. if results.status_code in [200, 201, 202]:
  191. changed = True
  192. return {'data': results.json(), 'changed': changed, 'status_code': results.status_code}
  193. # pylint: disable=too-many-branches
  194. def main():
  195. ''' ansible oc module for secrets '''
  196. module = AnsibleModule(
  197. argument_spec=dict(
  198. state=dict(default='offer', choices=['offer', 'cancel_op', 'go_live', 'publish']),
  199. force=dict(default=False, type='bool'),
  200. publisher=dict(default='redhat', type='str'),
  201. debug=dict(default=False, type='bool'),
  202. tenant_id=dict(default=os.environ.get('AZURE_TENANT_ID'), type='str'),
  203. client_id=dict(default=os.environ.get('AZURE_CLIENT_ID'), type='str'),
  204. client_secret=dict(default=os.environ.get('AZURE_CLIENT_SECRET'), type='str'),
  205. offer=dict(default=None, type='str'),
  206. offer_data=dict(default=None, type='dict'),
  207. emails=dict(default=None, type='list'),
  208. ),
  209. required_if=[
  210. ["state", "offer", ["offer_data"]],
  211. ],
  212. )
  213. # Verify we recieved either a valid key or edits with valid keys when receiving a src file.
  214. # A valid key being not None or not ''.
  215. if (module.params['tenant_id'] is None or module.params['client_id'] is None or
  216. module.params['client_secret'] is None):
  217. return module.fail_json(**{'failed': True,
  218. 'msg': 'Please specify tenant_id, client_id, and client_secret'})
  219. rval = AzurePublisher.run_ansible(module.params)
  220. if int(rval['status_code']) >= 300:
  221. rval['msg'] = 'Failed. status_code {}'.format(rval['status_code'])
  222. return module.fail_json(**rval)
  223. return module.exit_json(**rval)
  224. if __name__ == '__main__':
  225. main()