Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

# Copyright 2010 Jacob Kaplan-Moss 

# Copyright 2011 OpenStack LLC. 

# Copyright 2011 Piston Cloud Computing, Inc. 

# Copyright 2011 Nebula, Inc. 

 

# All Rights Reserved. 

""" 

OpenStack Client interface. Handles the REST calls and responses. 

""" 

 

import copy 

import logging 

import sys 

import urlparse 

 

import requests 

 

try: 

    import json 

except ImportError: 

    import simplejson as json 

 

# Python 2.5 compat fix 

25if not hasattr(urlparse, 'parse_qsl'): 

    import cgi 

    urlparse.parse_qsl = cgi.parse_qsl 

 

 

from keystoneclient import access 

from keystoneclient import exceptions 

 

 

_logger = logging.getLogger(__name__) 

 

 

def try_import_keyring(): 

    try: 

        import keyring 

        import pickle 

        return True 

    except ImportError: 

        if (hasattr(sys.stderr, 'isatty') and sys.stderr.isatty()): 

            print >> sys.stderr, 'Failed to load keyring modules.' 

        else: 

            _logger.warning('Failed to load keyring modules.') 

        return False 

 

 

class HTTPClient(object): 

 

    USER_AGENT = 'python-keystoneclient' 

 

    def __init__(self, username=None, tenant_id=None, tenant_name=None, 

                 password=None, auth_url=None, region_name=None, timeout=None, 

                 endpoint=None, token=None, cacert=None, key=None, 

                 cert=None, insecure=False, original_ip=None, debug=False, 

                 auth_ref=None, use_keyring=False, force_new_token=False, 

                 stale_duration=None): 

        """Construct a new http client 

 

        @param: timeout the request libary timeout in seconds (default None) 

 

        """ 

        self.version = 'v2.0' 

        # set baseline defaults 

        self.username = None 

        self.tenant_id = None 

        self.tenant_name = None 

        self.auth_url = None 

        self.management_url = None 

        if timeout is not None: 

            self.timeout = float(timeout) 

        else: 

            self.timeout = None 

        # if loading from a dictionary passed in via auth_ref, 

        # load values from AccessInfo parsing that dictionary 

        self.auth_ref = access.AccessInfo(**auth_ref) if auth_ref else None 

        if self.auth_ref: 

            self.username = self.auth_ref.username 

            self.tenant_id = self.auth_ref.tenant_id 

            self.tenant_name = self.auth_ref.tenant_name 

            self.auth_url = self.auth_ref.auth_url[0] 

            self.management_url = self.auth_ref.management_url[0] 

        # allow override of the auth_ref defaults from explicit 

        # values provided to the client 

        if username: 

            self.username = username 

        if tenant_id: 

            self.tenant_id = tenant_id 

        if tenant_name: 

            self.tenant_name = tenant_name 

        if auth_url: 

            self.auth_url = auth_url.rstrip('/') 

        if token: 

            self.auth_token_from_user = token 

        else: 

            self.auth_token_from_user = None 

        if endpoint: 

            self.management_url = endpoint.rstrip('/') 

        self.password = password 

        self.original_ip = original_ip 

        self.region_name = region_name 

        if cacert: 

            self.verify_cert = cacert 

        else: 

            self.verify_cert = True 

108        if insecure: 

            self.verify_cert = False 

        self.cert = cert 

        if cert and key: 

            self.cert = (cert, key,) 

        self.domain = '' 

 

        # logging setup 

        self.debug_log = debug 

117        if self.debug_log and not _logger.handlers: 

            ch = logging.StreamHandler() 

            _logger.setLevel(logging.DEBUG) 

            _logger.addHandler(ch) 

            if hasattr(requests, 'logging'): 

                requests.logging.getLogger(requests.__name__).addHandler(ch) 

 

        # keyring setup 

        self.use_keyring = use_keyring and try_import_keyring() 

        self.force_new_token = force_new_token 

        self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION 

        self.stale_duration = int(self.stale_duration) 

 

    @property 

    def auth_token(self): 

        if self.auth_token_from_user: 

            return self.auth_token_from_user 

exit        if self.auth_ref: 

            if self.auth_ref.will_expire_soon(self.stale_duration): 

                self.authenticate() 

            return self.auth_ref.auth_token 

 

    @auth_token.setter 

    def auth_token(self, value): 

        self.auth_token_from_user = value 

 

    @auth_token.deleter 

    def auth_token(self): 

        del self.auth_token_from_user 

 

    def authenticate(self, username=None, password=None, tenant_name=None, 

                     tenant_id=None, auth_url=None, token=None): 

        """ Authenticate user. 

 

        Uses the data provided at instantiation to authenticate against 

        the Keystone server. This may use either a username and password 

        or token for authentication. If a tenant name or id was provided 

        then the resulting authenticated client will be scoped to that 

        tenant and contain a service catalog of available endpoints. 

 

        With the v2.0 API, if a tenant name or ID is not provided, the 

        authenication token returned will be 'unscoped' and limited in 

        capabilities until a fully-scoped token is acquired. 

 

        If successful, sets the self.auth_ref and self.auth_token with 

        the returned token. If not already set, will also set 

        self.management_url from the details provided in the token. 

 

        :returns: ``True`` if authentication was successful. 

        :raises: AuthorizationFailure if unable to authenticate or validate 

                 the existing authorization token 

        :raises: ValueError if insufficient parameters are used. 

 

        If keyring is used, token is retrieved from keyring instead. 

        Authentication will only be necessary if any of the following 

        conditions are met: 

 

        * keyring is not used 

        * if token is not found in keyring 

        * if token retrieved from keyring is expired or about to 

          expired (as determined by stale_duration) 

        * if force_new_token is true 

 

        """ 

        auth_url = auth_url or self.auth_url 

        username = username or self.username 

        password = password or self.password 

        tenant_name = tenant_name or self.tenant_name 

        tenant_id = tenant_id or self.tenant_id 

 

192        if not token: 

            token = self.auth_token_from_user 

190            if (not token and self.auth_ref 

                and not self.auth_ref.will_expire_soon(self.stale_duration)): 

                token = self.auth_ref.auth_token 

 

        (keyring_key, auth_ref) = self.get_auth_ref_from_keyring(auth_url, 

                                                                 username, 

                                                                 tenant_name, 

                                                                 tenant_id, 

                                                                 token) 

        new_token_needed = False 

208        if auth_ref is None or self.force_new_token: 

            new_token_needed = True 

            raw_token = self.get_raw_token_from_identity_service(auth_url, 

                                                                 username, 

                                                                 password, 

                                                                 tenant_name, 

                                                                 tenant_id, 

                                                                 token) 

            self.auth_ref = access.AccessInfo(**raw_token) 

        else: 

            self.auth_ref = auth_ref 

        self.process_token() 

212        if new_token_needed: 

            self.store_auth_ref_into_keyring(keyring_key) 

        return True 

 

    def _build_keyring_key(self, auth_url, username, tenant_name, 

                           tenant_id, token): 

        """ Create a unique key for keyring. 

 

        Used to store and retrieve auth_ref from keyring. 

 

        """ 

        keys = [auth_url, username, tenant_name, tenant_id, token] 

        for index, key in enumerate(keys): 

            if key is None: 

                keys[index] = '?' 

        keyring_key = '/'.join(keys) 

        return keyring_key 

 

    def get_auth_ref_from_keyring(self, auth_url, username, tenant_name, 

                                  tenant_id, token): 

        """ Retrieve auth_ref from keyring. 

 

        If auth_ref is found in keyring, (keyring_key, auth_ref) is returned. 

        Otherwise, (keyring_key, None) is returned. 

 

        :returns: (keyring_key, auth_ref) or (keyring_key, None) 

 

        """ 

        keyring_key = None 

        auth_ref = None 

241        if self.use_keyring: 

            keyring_key = self._build_keyring_key(auth_url, username, 

                                                  tenant_name, tenant_id, 

                                                  token) 

            try: 

                auth_ref = keyring.get_password("keystoneclient_auth", 

                                                keyring_key) 

                if auth_ref: 

                    auth_ref = pickle.loads(auth_ref) 

                    if self.auth_ref.will_expire_soon(self.stale_duration): 

                        # token has expired, don't use it 

                        auth_ref = None 

            except Exception as e: 

                auth_ref = None 

                _logger.warning('Unable to retrieve token from keyring %s' % ( 

                    e)) 

        return (keyring_key, auth_ref) 

 

    def store_auth_ref_into_keyring(self, keyring_key): 

        """ Store auth_ref into keyring. 

 

        """ 

263        if self.use_keyring: 

            try: 

                keyring.set_password("keystoneclient_auth", 

                                     keyring_key, 

                                     pickle.dumps(self.auth_ref)) 

            except Exception as e: 

                _logger.warning("Failed to store token into keyring %s" % (e)) 

 

    def process_token(self): 

        """ Extract and process information from the new auth_ref. 

 

        """ 

        raise NotImplementedError 

 

    def get_raw_token_from_identity_service(self, auth_url, username=None, 

                                            password=None, tenant_name=None, 

                                            tenant_id=None, token=None): 

        """ Authenticate against the Identity API and get a token. 

 

        Not implemented here because auth protocols should be API 

        version-specific. 

 

        Expected to authenticate or validate an existing authentication 

        reference already associated with the client. Invoking this call 

        *always* makes a call to the Keystone. 

 

        :returns: ``raw token`` 

 

        """ 

        raise NotImplementedError 

 

    def _extract_service_catalog(self, url, body): 

        """ Set the client's service catalog from the response data. 

 

        Not implemented here because data returned may be API 

        version-specific. 

        """ 

        raise NotImplementedError 

 

    def http_log_req(self, args, kwargs): 

305        if not self.debug_log: 

            return 

 

        string_parts = ['curl -i'] 

        for element in args: 

            if element in ('GET', 'POST'): 

                string_parts.append(' -X %s' % element) 

            else: 

                string_parts.append(' %s' % element) 

 

        for element in kwargs['headers']: 

            header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) 

            string_parts.append(header) 

 

        _logger.debug("REQ: %s" % "".join(string_parts)) 

        if 'data' in kwargs: 

            _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) 

 

    def http_log_resp(self, resp): 

322        if self.debug_log: 

            _logger.debug( 

                "RESP: [%s] %s\nRESP BODY: %s\n", 

                resp.status_code, 

                resp.headers, 

                resp.text) 

 

    def serialize(self, entity): 

        return json.dumps(entity) 

 

    def request(self, url, method, **kwargs): 

        """ Send an http request with the specified characteristics. 

 

        Wrapper around requests.request to handle tasks such as 

        setting headers, JSON encoding/decoding, and error handling. 

        """ 

        # Copy the kwargs so we can reuse the original in case of redirects 

        request_kwargs = copy.copy(kwargs) 

        request_kwargs.setdefault('headers', kwargs.get('headers', {})) 

        request_kwargs['headers']['User-Agent'] = self.USER_AGENT 

        if self.original_ip: 

            request_kwargs['headers']['Forwarded'] = "for=%s;by=%s" % ( 

                self.original_ip, self.USER_AGENT) 

        if 'body' in kwargs: 

            request_kwargs['headers']['Content-Type'] = 'application/json' 

            request_kwargs['data'] = self.serialize(kwargs['body']) 

            del request_kwargs['body'] 

        if self.cert: 

            request_kwargs['cert'] = self.cert 

        if self.timeout is not None: 

            request_kwargs.setdefault('timeout', self.timeout) 

 

        self.http_log_req((url, method,), request_kwargs) 

 

        try: 

            resp = requests.request( 

                method, 

                url, 

                verify=self.verify_cert, 

                **request_kwargs) 

        except requests.ConnectionError: 

            msg = 'Unable to establish connection to %s' % url 

            raise exceptions.ClientException(msg) 

 

        self.http_log_resp(resp) 

 

        if resp.text: 

            try: 

                body = json.loads(resp.text) 

            except (ValueError, TypeError): 

                body = None 

                _logger.debug("Could not decode JSON from body: %s" 

                              % resp.text) 

        else: 

            _logger.debug("No body was returned.") 

            body = None 

 

        if resp.status_code >= 400: 

            _logger.debug( 

                "Request returned failure status: %s", 

                resp.status_code) 

            raise exceptions.from_response(resp, body or resp.text) 

        elif resp.status_code in (301, 302, 305): 

            # Redirected. Reissue the request to the new location. 

            return self.request(resp.headers['location'], method, **kwargs) 

 

        return resp, body 

 

    def _cs_request(self, url, method, **kwargs): 

        """ Makes an authenticated request to keystone endpoint by 

        concatenating self.management_url and url and passing in method and 

        any associated kwargs. """ 

 

        is_management = kwargs.pop('management', True) 

 

        if is_management and self.management_url is None: 

            raise exceptions.AuthorizationFailure( 

                'Current authorization does not have a known management url') 

 

        url_to_use = self.auth_url 

        if is_management: 

            url_to_use = self.management_url 

 

        kwargs.setdefault('headers', {}) 

408        if self.auth_token: 

            kwargs['headers']['X-Auth-Token'] = self.auth_token 

 

        resp, body = self.request(url_to_use + url, method, 

                                  **kwargs) 

        return resp, body 

 

    def get(self, url, **kwargs): 

        return self._cs_request(url, 'GET', **kwargs) 

 

    def head(self, url, **kwargs): 

        return self._cs_request(url, 'HEAD', **kwargs) 

 

    def post(self, url, **kwargs): 

        return self._cs_request(url, 'POST', **kwargs) 

 

    def put(self, url, **kwargs): 

        return self._cs_request(url, 'PUT', **kwargs) 

 

    def patch(self, url, **kwargs): 

        return self._cs_request(url, 'PATCH', **kwargs) 

 

    def delete(self, url, **kwargs): 

        return self._cs_request(url, 'DELETE', **kwargs)