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
defmodule ExOvh.Hubic.OpenstackCache do
@moduledoc """
Caches the openstack credentials for access to the openstack api
Hubic does not use the standard Openstack Identity (Keystone) api for auth.
"""
use GenServer
alias ExOvh.Hubic.TokenCache
alias ExOvh.Hubic.Request
@get_credentials_retries 10
@get_credentials_sleep_interval 150
#####################
# Public
#####################
@doc "Starts the genserver"
def start_link({client, config, opts}) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
GenServer.start_link(__MODULE__, {client, config, opts}, [name: gen_server_name(client)])
end
def get_credentials(), do: get_credentials(ExOvh)
def get_credentials(client), do: get_credentials(client, 0)
def get_credentials_token(), do: get_credentials_token(ExOvh)
def get_credentials_token(client), do: get_credentials(client)["token"]
def get_endpoint(), do: get_endpoint(ExOvh)
def get_endpoint(client) do
credentials = get_credentials(client)
path = URI.parse(credentials["endpoint"])
|> Map.get(:path)
{version, account} = String.split_at(path, 4)
endpoint = List.first(String.split(credentials["endpoint"], account))
endpoint
end
def get_account(), do: get_account(ExOvh)
def get_account(client) do
credentials = get_credentials(client)
path = URI.parse(credentials["endpoint"])
|> Map.get(:path)
{version, account} = String.split_at(path, 4)
account
end
#####################
# Genserver Callbacks
#####################
# trap exits so that terminate callback is invoked
# the :lock key is to allow for locking during the brief moment that the access token is being refreshed
def init({client, config, opts}) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
:erlang.process_flag(:trap_exit, :true)
token = TokenCache.get_token(client)
|> LoggingUtils.log_return(:debug)
:timer.sleep(10_000) # give some time for TokenCache Genserver to initialize
create_ets_table(client)
credentials = Request.request(client, :get, "/account/credentials", "")
|> LoggingUtils.log_return
|> Map.put(:lock, :false)
|> LoggingUtils.log_return
:ets.insert(ets_tablename(client), {:credentials, credentials})
expires = to_seconds(credentials["expires"])
Task.start_link(fn -> monitor_expiry(client, expires) end)
{:ok, {client, config, credentials}}
end
def handle_call(:add_lock, _from, {client, config, credentials}) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
new_credentials = Map.put(credentials, :lock, :true)
:ets.insert(ets_tablename(client), {:credentials, new_credentials})
{:reply, :ok, {client, config, new_credentials}}
end
def handle_call(:remove_lock, _from, {client, config, credentials}) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
new_credentials = Map.put(credentials, :lock, :false)
:ets.insert(ets_tablename(client), {:credentials, new_credentials})
{:reply, :ok, {client, config, new_credentials}}
end
def handle_call(:update_credentials, _from, {client, config, credentials}) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
new_credentials = Request.request(client, :get, "/account/credentials", "")
|> Map.put(credentials, :lock, :false)
:ets.insert(ets_tablename(client), {:credentials, new_credentials})
{:reply, :ok, {client, config, new_credentials}}
end
def handle_call(:stop, _from, state) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
{:stop, :shutdown, :ok, state}
end
def terminate(:shutdown, {client, config, credentials}) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
:ets.delete(ets_tablename(client)) # explicilty remove
:ok
end
#####################
# Private
#####################
defp gen_server_name(client), do: String.to_atom(Atom.to_string(client) <> Atom.to_string(__MODULE__))
defp ets_tablename(client), do: String.to_atom("Ets" <> Atom.to_string(gen_server_name(client)))
defp get_credentials(client, index) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
if ets_tablename(client) in :ets.all() do
[credentials: credentials] = :ets.lookup(ets_tablename(client), :credentials)
if credentials.lock === :true do
if index > @get_credentials_retries do
raise "Problem retrieving the openstack credentials from ets table"
else
:timer.sleep(@get_credentials_sleep_interval)
get_credentials(index + 1)
end
else
credentials
end
else
if index > @get_credentials_retries do
raise "Problem retrieving the openstack credentials from ets table"
else
:timer.sleep(@get_credentials_sleep_interval)
get_credentials(index + 1)
end
end
end
defp monitor_expiry(client, expires) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
interval = (expires - 30) * 1000
:timer.sleep(interval)
LoggingUtils.log_return("monitor_expiry_openstack_credentials task is fetching new openstack credentials #{ets_tablename(client)}")
{:reply, :ok, _credentials} = GenServer.call(gen_server_name(client), :add_lock)
{:reply, :ok, _credentials} = GenServer.call(gen_server_name(client), :update_credentials)
{:reply, :ok, credentials} = GenServer.call(gen_server_name(client), :remove_lock)
expires = to_seconds(credentials["expires"])
monitor_expiry(client, expires)
end
defp create_ets_table(client) do
LoggingUtils.log_mod_func_line(__ENV__, :debug)
ets_options = [
:set, # type
:protected, # read - all, write this process only.
:named_table,
{:heir, :none}, # don't let any process inherit the table. when the ets table dies, it dies.
{:write_concurrency, :false},
{:read_concurrency, :true}
]
unless ets_tablename(client) in :ets.all() do
:ets.new(ets_tablename(client), ets_options)
end
end
defp to_seconds(iso_time) do
{:ok, expiry_ndt, offset} = Calendar.NaiveDateTime.Parse.iso8601(iso_time)
{:ok, expiry_dt_utc} = Calendar.NaiveDateTime.with_offset_to_datetime_utc(expiry_ndt, offset)
{:ok, now} = Calendar.DateTime.from_erl(:calendar.universal_time(), "UTC")
{:ok, seconds, _microseconds, _when} = Calendar.DateTime.diff(expiry_dt_utc, now)
if seconds > 0 do
seconds
else
0
end
end
end