client.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import copy
  2. import logging
  3. import os
  4. import json
  5. import httplib2
  6. from .tenantinfo import TenantInfo, TenantInfoManager
  7. from . import exceptions
  8. USER_AGENT = "ocboot-python-clien"
  9. logger = logging.getLogger(__name__)
  10. class HTTPClient(httplib2.Http):
  11. def __init__(self, timeout, insecure):
  12. super(HTTPClient, self).__init__(
  13. timeout=timeout,
  14. disable_ssl_certificate_validation=insecure)
  15. # httplib2 overrides
  16. self.force_exception_to_status_code = True
  17. def http_log(self, args, kwargs, resp, body):
  18. if os.environ.get('YUNIONCLIENT_DEBUG', True):
  19. ch = logging.StreamHandler()
  20. logger.setLevel(logging.DEBUG)
  21. logger.addHandler(ch)
  22. elif not logger.isEnabledFor(logging.DEBUG):
  23. return
  24. string_parts = ['curl -i']
  25. for element in args:
  26. if element in ('GET', 'POST'):
  27. string_parts.append(' -X %s' % element)
  28. else:
  29. string_parts.append(' %s' % element)
  30. for element in kwargs['headers']:
  31. header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
  32. string_parts.append(header)
  33. logger.debug("REQ: %s\n" % "".join(string_parts))
  34. if 'raw_body' in kwargs:
  35. logger.debug("REQ BODY (RAW): %s\n" % (kwargs['raw_body']))
  36. if 'body' in kwargs:
  37. logger.debug("REQ BODY: %s\n" % (kwargs['body']))
  38. logger.debug("RESP: %s\nRESP BODY: %s\n", resp, body)
  39. def _strip_version(self, endpoint):
  40. """Strip a version from the last component of an endpoint if present"""
  41. # Get rid of trailing '/' if present
  42. while endpoint.endswith('/'):
  43. endpoint = endpoint[:-1]
  44. url_bits = endpoint.split('/')
  45. # regex to match 'v1' or 'v2.0' etc
  46. import re
  47. if re.match('v\d+\.?\d*', url_bits[-1]):
  48. endpoint = '/'.join(url_bits[:-1])
  49. return endpoint
  50. def _get_urllib2_raw_request(self, endpoint, auth_token, method, url,
  51. **kwargs):
  52. import urllib.request, urllib.error, urllib.parse
  53. url = endpoint + url
  54. url = url.encode('UTF-8')
  55. req = urllib.request.Request(url)
  56. headers = copy.deepcopy(kwargs.get('headers', {}))
  57. headers.setdefault('User-Agent', USER_AGENT)
  58. headers.setdefault('Connection', 'Close')
  59. if auth_token:
  60. headers.setdefault('X-Auth-Token', auth_token)
  61. for h in list(headers.keys()):
  62. req.add_header(h, headers[h])
  63. if 'body' in kwargs:
  64. req.add_data(kwargs['body'])
  65. return urllib.request.urlopen(req)
  66. def _http_request(self, endpoint, auth_token, url, method, **kwargs):
  67. """ Send an http request with the specified characteristics.
  68. Wrapper around httplib2.Http.request to handle tasks such as
  69. setting headers, JSON encoding/decoding, and error handling.
  70. """
  71. url = endpoint + url
  72. # Copy the kwargs so we can reuse the original in case of redirects
  73. kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
  74. kwargs['headers'].setdefault('User-Agent', USER_AGENT)
  75. #kwargs['headers'].setdefault('Accept-Encoding', 'identity')
  76. if auth_token:
  77. kwargs['headers'].setdefault('X-Auth-Token', auth_token)
  78. if 'body' not in kwargs and method in ['POST', 'PUT']:
  79. kwargs['headers'].setdefault('Content-length', '0')
  80. #print url
  81. #print kwargs['headers']
  82. resp, body = super(HTTPClient, self).request(url, method, **kwargs)
  83. self.http_log((url, method,), kwargs, resp, body)
  84. #print resp
  85. #print 'BODY', body
  86. if 400 <= resp.status < 600:
  87. #logger.exception("Request returned failure status.")
  88. raise exceptions.from_response(resp, body)
  89. elif resp.status in (301, 302, 305):
  90. # Redirected. Reissue the request to the new location.
  91. return self._http_request(resp['location'], method, **kwargs)
  92. return resp, body
  93. def _json_request(self, endpoint, auth_token, method, url, **kwargs):
  94. kwargs.setdefault('headers', {})
  95. if 'body' in kwargs and kwargs['body'] is not None:
  96. kwargs['headers'].setdefault('Content-Type', 'application/json')
  97. kwargs['body'] = json.dumps(kwargs['body'])
  98. resp, body = self._http_request(endpoint, auth_token, url, method,
  99. **kwargs)
  100. if body:
  101. try:
  102. body = json.loads(body)
  103. except ValueError:
  104. logger.debug("Could not decode JSON from body: %s" % body)
  105. else:
  106. logger.debug("No body was returned.")
  107. body = None
  108. return resp, body
  109. def _raw_request(self, endpoint, auth_token, method, url, **kwargs):
  110. kwargs.setdefault('headers', {})
  111. if 'body' in kwargs and kwargs['body'] is not None:
  112. kwargs['headers'].setdefault('Content-Type',
  113. 'application/octet-stream')
  114. return self._http_request(endpoint, auth_token, url, method, **kwargs)
  115. class Client(HTTPClient):
  116. def __init__(self, auth_url, username, password, domain_name,
  117. region=None, zone=None, endpoint_type='publicURL',
  118. timeout=600, insecure=False):
  119. super(Client, self).__init__(timeout, insecure)
  120. self.auth_url = auth_url
  121. self.username = username
  122. self.password = password
  123. self.domain_name = domain_name
  124. self.endpoint_type = endpoint_type
  125. self.set_region(region, zone)
  126. self.default_tenant = None
  127. self.tenants_info_manager = TenantInfoManager()
  128. def set_region(self, region, zone=None):
  129. self.region = region
  130. self.zone = zone
  131. def _authenticatev3(self, project_name=None, project_id=None):
  132. logging.info('authenticate %s %s' % (project_name, project_id))
  133. auth = {}
  134. user = {'name': self.username, 'password': self.password}
  135. if self.domain_name:
  136. user['domain'] = {'name': self.domain_name}
  137. else:
  138. user['domain'] = {'id': 'default'}
  139. auth['identity'] = {'methods': ['password'],
  140. 'password': {'user': user}}
  141. project = {}
  142. if project_name:
  143. project['name'] = project_name
  144. project['domain'] = {'id': 'default'}
  145. if project_id:
  146. project['id'] = project_id
  147. auth['scope'] = {'project': project}
  148. body = {'auth': auth}
  149. resp, body = self._json_request(self.auth_url, None,
  150. 'POST', '/auth/tokens', body=body)
  151. if 'token' in body:
  152. token_id = resp['x-subject-token']
  153. if 'project' in body['token']:
  154. self.default_tenant = TenantInfo(None, None)
  155. token = {'id': token_id,
  156. 'tenant': body['token']['project'],
  157. 'expires': body['token']['expires_at']}
  158. catalog = body['token']['catalog']
  159. user = body['token']['user']
  160. self.default_tenant.set_access_info(token, catalog, user)
  161. self.tenants_info_manager.add_tenant(self.default_tenant)
  162. else:
  163. self._fetch_tenants(token_id)
  164. return True
  165. else:
  166. raise Exception('Wrong return format %s' % json.dumps(body))
  167. def _authenticate(self, tenant_name=None, tenant_id=None):
  168. logging.info('authenticate %s %s' % (tenant_name, tenant_id))
  169. auth = {}
  170. auth['passwordCredentials'] = {'username': self.username,
  171. 'password': self.password}
  172. if tenant_id is not None and len(tenant_id) > 0:
  173. auth['tenantId'] = tenant_id
  174. elif tenant_name is not None and len(tenant_name) > 0:
  175. auth['tenantName'] = tenant_name
  176. body = {'auth': auth}
  177. resp, body = self._json_request(self.auth_url, None,
  178. 'POST', '/tokens', body=body)
  179. # print json.dumps(body, indent=4)
  180. if 'access' in body:
  181. token = body['access']['token']
  182. catalog = body['access']['serviceCatalog']
  183. user = body['access']['user']
  184. if 'tenant' in token:
  185. self.default_tenant = TenantInfo(None, None)
  186. # print 'Token:', token
  187. self.default_tenant.set_access_info(token, catalog, user)
  188. self.tenants_info_manager.add_tenant(self.default_tenant)
  189. else:
  190. self._fetch_tenants(token['id'])
  191. return True
  192. else:
  193. raise Exception('Wrong return format %s' % json.dumps(body))
  194. return False
  195. def _fetch_tenants(self, token):
  196. try:
  197. resp, body = self._json_request(self.auth_url, token,
  198. 'GET', '/tenants')
  199. if 'tenants' in body:
  200. for t in body['tenants']:
  201. self.tenants_info_manager.add_tenant(TenantInfo(t['id'],
  202. t['name']))
  203. return True
  204. except Exception as e:
  205. raise Exception('_fetch_tenants %s' % e)
  206. return False
  207. def get_tenants(self):
  208. self._authenticate(None, None)
  209. return self.tenants_info_manager.get_tenants()
  210. def set_project(self, project_name=None, project_id=None):
  211. return self.set_tenant(tenant_name=project_name, tenant_id=project_id)
  212. def set_tenant(self, tenant_name=None, tenant_id=None):
  213. tenant = self.tenants_info_manager.get_tenant(tenant_id=tenant_id,
  214. tenant_name=tenant_name)
  215. if tenant is None:
  216. return self._authenticatev3(project_name=tenant_name,
  217. project_id=tenant_id)
  218. else:
  219. self.default_tenant = tenant
  220. return True
  221. def get_default_tenant(self):
  222. if self.default_tenant is None:
  223. raise Exception('No tenant specified')
  224. # if self.default_tenant.expire_soon():
  225. # self._authenticate(tenant_name=self.default_tenant.get_name(),
  226. # tenant_id=self.default_tenant.get_id())
  227. return self.default_tenant
  228. def get_regions(self):
  229. t = self.get_default_tenant()
  230. if t is not None:
  231. return t.get_regions()
  232. else:
  233. return None
  234. def get_endpoint(self, service, admin_api=False, region=None, zone=None):
  235. t = self.get_default_tenant()
  236. if t is not None:
  237. if admin_api:
  238. ep_type = 'adminURL'
  239. else:
  240. ep_type = self.endpoint_type
  241. if region is None:
  242. region = self.region
  243. if zone is None:
  244. zone = self.zone
  245. return t.get_endpoint(region, service, ep_type, zone=zone)
  246. else:
  247. raise Exception('No tenant specified')
  248. def _wrapped_request(self, func, service, admin_api, method, url, **kwargs):
  249. t = self.get_default_tenant()
  250. if t is not None:
  251. ep = self.get_endpoint(service, admin_api)
  252. if ep is not None:
  253. ep = self._strip_version(ep)
  254. return func(ep, t.get_token(), method, url, **kwargs)
  255. else:
  256. raise Exception('NO valid endpoint found for %s' % service)
  257. else:
  258. raise Exception('No tenant specified')
  259. def json_request(self, service, admin_api, method, url, **kwargs):
  260. return self._wrapped_request(self._json_request, service, admin_api,
  261. method, url, **kwargs)
  262. def raw_request(self, service, admin_api, method, url, **kwargs):
  263. return self._wrapped_request(self._raw_request, service, admin_api,
  264. method, url, **kwargs)
  265. def get_urllib2_raw_request(self, service, admin_api, url, **kwargs):
  266. return self._wrapped_request(self._get_urllib2_raw_request, service,
  267. admin_api, 'GET', url, **kwargs)
  268. def from_file(self, filename):
  269. with open(filename, 'r') as f:
  270. desc = f.read()
  271. self.from_json(json.loads(desc))
  272. def from_json(self, desc):
  273. self.auth_url = desc['auth_url']
  274. self.username = desc['username']
  275. self.endpoint_type = desc['endpoint_type']
  276. self.set_region(desc['region'], desc.get('zone', None))
  277. self.tenants_info_manager = TenantInfoManager()
  278. self.tenants_info_manager.from_json(desc['tenants'])
  279. if 'default_tenant_id' in desc:
  280. self.set_tenant(tenant_id=desc['default_tenant_id'])
  281. def to_file(self, filename):
  282. with open(filename, 'w') as f:
  283. desc = self.to_json()
  284. f.write(json.dumps(desc))
  285. def to_json(self):
  286. desc = {}
  287. desc['tenants'] = self.tenants_info_manager.to_json()
  288. desc['username'] = self.username
  289. desc['auth_url'] = self.auth_url
  290. desc['region'] = self.region
  291. if self.zone:
  292. desc['zone'] = self.zone
  293. desc['endpoint_type'] = self.endpoint_type
  294. if self.default_tenant is not None:
  295. desc['default_tenant_id'] = self.default_tenant.get_id()
  296. return desc
  297. def is_admin(self):
  298. tenant = self.get_default_tenant()
  299. if tenant is not None:
  300. return tenant.is_admin()
  301. return False
  302. def is_system_admin(self):
  303. tenant = self.get_default_tenant()
  304. if tenant is not None:
  305. return tenant.is_system_admin()
  306. return False
  307. if __name__ == '__main__':
  308. desc = {
  309. 'project_name': 'system',
  310. 'project_id': None,
  311. 'args': (
  312. 'https://10.1.2.56:30500/v3',
  313. 'sysadmin',
  314. 'HXf7J7zAw59FpAJz',
  315. None,
  316. ),
  317. 'kwargs': {
  318. 'region': 'region0',
  319. 'zone': None,
  320. 'insecure': True,
  321. 'endpoint_type': 'publicURL',
  322. }
  323. }
  324. args = desc['args']
  325. kwargs = desc['kwargs']
  326. client = Client(*args, *kwargs)
  327. client.set_project(desc.get('project_name'))