1919 terminate /2
2020]).
2121
22+
2223-record (state , {
2324 scanned_time ,
2425 license_type ,
2728 expiration_date ,
2829 message ,
2930 customer_name ,
30- license_id
31+ license_id ,
32+ install_time
3133}).
3234
3335-define (DEFAULT_LICENSE_SCAN_INTERVAL , 30000 ). % milli seconds
34-
3536-define (DEFAULT_FILE_PATH , " /tmp/lic" ).
36-
3737-define (LICENSE_SCAN_CMD , " chef-automate license status --result-json " ).
3838
3939
40+ % % Valid options: cli, or a path to a file
41+ % % Note that this is defaulted to cli in erl_opts in rebar.config.script but can be overridden
42+ % % with the path at build time by setting OC_LICENSE_PATH=<path> in the environment.
43+ % % (In that case, path should be something like /var/opt/opscode/license.lic)
44+ -ifndef (OC_LICENSE_PATH ).
45+ -define (OC_LICENSE_PATH , cli ).
46+ -endif .
47+
4048% %% ======================================
4149% %% Exported
4250% %% ======================================
@@ -51,7 +59,7 @@ get_license() ->
5159% %% Gen Server callbacks
5260% %% ======================================
5361init (_Config ) ->
54- State = check_license (# state {} ),
62+ State = check_license (# state {install_time = install_time ()}, ? OC_LICENSE_PATH ),
5563 erlang :send_after (? DEFAULT_LICENSE_SCAN_INTERVAL , self (), check_license ),
5664 {ok , State }.
5765
@@ -66,7 +74,7 @@ handle_cast(_Message, State) ->
6674 {noreply , State }.
6775
6876handle_info (check_license , State ) ->
69- State1 = check_license (State ),
77+ State1 = check_license (State , ? OC_LICENSE_PATH ),
7078 erlang :send_after (? DEFAULT_LICENSE_SCAN_INTERVAL , self (), check_license ),
7179 {noreply , State1 };
7280
@@ -79,35 +87,147 @@ code_change(_OldVsn, State) ->
7987terminate (_Reason , _State ) ->
8088 ok .
8189
90+ % %%
91+ % %% Internal Implementation
92+ % %%
93+
94+ % % Return an approximate installation time based on the unix timestamp found kvpairs
95+ install_time () ->
96+ DefaultTime = os :system_time (second ),
97+ case chef_sql :select_rows ({value_for_key , [<<" itime" >>]}) of
98+ [[{<<" value" >>, Itime }]]->
99+ try
100+ binary_to_integer (Itime )
101+ catch error :badarg ->
102+ DefaultTime
103+ end ;
104+ not_found -> {ok , 0 };
105+ {error , Reason } -> {error , Reason }
106+ end .
107+
108+ % This will continue to be the default behavior - license will only be checked
109+ % as part of an automate install, when the automate CLI is present.
110+ get_license_info (_InstallTime , cli ) ->
111+ os :cmd (? LICENSE_SCAN_CMD ++ ? DEFAULT_FILE_PATH ),
112+ {ok , Bin } = file :read_file (? DEFAULT_FILE_PATH ),
113+ {Json } = jiffy :decode (Bin ),
114+ Json ;
115+ % This is for the file-based license mode, where we read the license directly from a file path.
116+ get_license_info (InstallTime , Path ) ->
117+ case load_local_license (InstallTime , Path ) of
118+ {ok , Json } ->
119+ io :format (" Data: ~p~n " , Json ),
120+ Json ;
121+ {error , Reason } ->
122+ io :format (" Failed to load local license going with default: ~p~n " , [Reason ]),
123+ make_license_payload (InstallTime , #{})
124+ end .
125+
126+ load_local_license (InstallTime , Loader ) when is_function (Loader ) ->
127+ case Loader () of
128+ {ok , Bin } ->
129+ % % Input format is the dotted three-part JWT style base64url encoded string, so
130+ % % we start by splitting on dot and ensuring we get the expected three parts
131+ case binary :split (Bin , [<<" ." >>], [trim_all , global ]) of
132+ % % NOTE: This implementation does not verify signature. To implement signature verification,
133+ % % we will need to have the license service public key available to the license worker
134+ % % at a known path.
135+ [_Header , Payload , _Signature ] ->
136+ try base64 :decode (Payload , #{ padding => false , mode => urlsafe } ) of
137+ Decoded ->
138+ try jiffy :decode (Decoded , [return_maps ]) of
139+ J -> {ok , make_license_payload (InstallTime , J )}
140+ catch _ :Reason ->
141+ { error , {invalid_json , Reason } } % %, Decoded } }
142+ end
143+ catch _ :Reason ->
144+ { error , { invalid_base64 , Reason } } % , Payload } }
145+ end ;
146+ _ ->
147+ { error , { invalid_license_format } }
148+ end ;
149+ {error , Reason } ->
150+ { error , { load_failed , Reason } }
151+ end ;
152+ load_local_license (InstallTime , Path ) ->
153+ load_local_license ( InstallTime , fun () -> file :read_file (Path ) end ).
154+
155+
156+ % % Generates a license payload using a parsed license. The generated license is 's compatiable with the
157+ % % automate-ctl license status json output object, so that we can use the same processing function for both
158+ % % the CLI and file-based license modes.
159+ % % In some cases, we don't have all the data that the license service would provide, so we'll make substitutes.
160+ make_license_payload (_InstallTime , #{ <<" id" >> := LicenseId ,
161+ <<" customer" >> := CustomerName ,
162+ <<" entitlements" >> := Entitlements ,
163+ <<" type" >> := LicenseType
164+ }) ->
165+
166+ % % The license server itself would give us actual expiration date. We have to
167+ % % approximate as best we can based the latest end date of available entitilements.
168+ ExpireInSeconds = case Entitlements of
169+ [] -> 0 ; % No entitlements, treat as expired or invalid license
170+ _ -> lists :max ([Ent || #{ <<" end" >> := #{<<" seconds" >> := Ent }} <- Entitlements ])
171+ end ,
172+
173+ [ {<<" result" >>, {[
174+ {<<" license_id" >>, LicenseId },
175+ {<<" customer_name" >>, CustomerName },
176+ {<<" expiration_date" >>, {[{<<" seconds" >>, ExpireInSeconds }]}},
177+ {<<" license_type" >>, LicenseType },
178+ {<<" grace_period" >>, false }
179+ ]}}
180+ ];
181+ make_license_payload (InstallTime , _Other ) ->
182+ % % If we don't have the expected fields, we don't have a valid license - so let's make something that looks like one,
183+ % % but is valid from the install date + 90 days.
184+ ExpirationTime = InstallTime + (60 * 60 * 24 * 90 ), % 90 days from install time
185+ lager :warning (" License payload is missing expected fields, treating as valid license with expiration : ~p " , [ExpirationTime ]),
186+ [ {<<" result" >>, {[
187+ {<<" license_id" >>, <<>>},
188+ {<<" customer_name" >>, <<>>},
189+ {<<" expiration_date" >>, {[{<<" seconds" >>, ExpirationTime }]}},
190+ {<<" license_type" >>, <<" commercial" >>},
191+ {<<" grace_period" >>, false }
192+ ]}}
193+ ].
194+
195+
196+
197+ % %%
82198% %% =====================
83199% %% Internal functions
84200% %% =====================
85- check_license (State ) ->
86- JsonStr =
87- case catch get_license_info () of
88- Result when is_list (Result ) -> Result ;
89- {'EXIT' , _ } -> <<" " >>
90- end ,
91- case process_license (JsonStr ) of
92- {ok , valid_license , ExpDate , CustomerName , LicenseId } ->
93- State # state {license_cache = valid_license , grace_period = undefined , scanned_time = erlang :timestamp (), expiration_date = ExpDate , customer_name = CustomerName , license_id = LicenseId };
94- {ok , commercial_expired , ExpDate , Msg , CustomerName , LicenseId } ->
95- State # state {license_cache = commercial_expired , license_type = <<" commercial" >>, grace_period = undefined , scanned_time = erlang :timestamp (), expiration_date = ExpDate , message = Msg , customer_name = CustomerName , license_id = LicenseId };
96- {ok , commercial_grace_period , ExpDate , Msg , CustomerName , LicenseId } ->
97- State # state {license_cache = commercial_grace_period , grace_period = true , scanned_time = erlang :timestamp (), expiration_date = ExpDate , message = Msg , customer_name = CustomerName , license_id = LicenseId };
98- {ok , trial_expired , ExpDate , Msg , CustomerName , LicenseId } ->
99- State # state {license_cache = trial_expired_expired , license_type = <<" trial" >>, grace_period = undefined , scanned_time = erlang :timestamp (), expiration_date = ExpDate , message = Msg , customer_name = CustomerName , license_id = LicenseId };
100- {error , no_license } ->
101- State # state {license_cache = trial_expired_expired , license_type = <<" trial" >>, grace_period = undefined , scanned_time = erlang :timestamp (), expiration_date = " " , message = get_alert_message (trial_expired , " " )};
102- {error , _ } -> State
103- end .
201+ check_license (# state {install_time = InstallTime } = State , ModeOrPath ) ->
202+ JsonStr = case catch get_license_info (InstallTime , ModeOrPath ) of
203+ Result when is_list (Result ) -> Result ;
204+ {'EXIT' , N } ->
205+ io :format (" Oopsie, exited license check ~p~n " , [N ]),
206+ <<" " >>
207+ end ,
208+ R2 = process_license (JsonStr ),
104209
105- get_license_info () ->
106- os :cmd (? LICENSE_SCAN_CMD ++ ? DEFAULT_FILE_PATH ),
107- {ok , Bin } = file :read_file (? DEFAULT_FILE_PATH ),
108- {JsonStr } = jiffy :decode (Bin ),
109- JsonStr .
210+ io :format (" process_license result: ~p ~p~n " , [JsonStr , R2 ]),
211+ case process_license (JsonStr ) of
212+ {ok , valid_license , ExpDate , CustomerName , LicenseId } ->
213+ State # state {license_cache = valid_license , grace_period = undefined , scanned_time = erlang :timestamp (), expiration_date = ExpDate , customer_name = CustomerName , license_id = LicenseId };
214+ {ok , commercial_expired , ExpDate , Msg , CustomerName , LicenseId } ->
215+ State # state {license_cache = commercial_expired , license_type = <<" commercial" >>, grace_period = undefined , scanned_time = erlang :timestamp (), expiration_date = ExpDate , message = Msg , customer_name = CustomerName , license_id = LicenseId };
216+ {ok , commercial_grace_period , ExpDate , Msg , CustomerName , LicenseId } ->
217+ State # state {license_cache = commercial_grace_period , grace_period = true , scanned_time = erlang :timestamp (), expiration_date = ExpDate , message = Msg , customer_name = CustomerName , license_id = LicenseId };
218+ {ok , trial_expired , ExpDate , Msg , CustomerName , LicenseId } ->
219+ State # state {license_cache = trial_expired_expired , license_type = <<" trial" >>, grace_period = undefined , scanned_time = erlang :timestamp (), expiration_date = ExpDate , message = Msg , customer_name = CustomerName , license_id = LicenseId };
220+ {error , no_license } ->
221+ State # state {license_cache = trial_expired_expired , license_type = <<" trial" >>, grace_period = undefined , scanned_time = erlang :timestamp (), expiration_date = " " , message = get_alert_message (trial_expired , " " )};
222+ {error , _ } -> State
223+ end .
110224
225+
226+ % missing license: gets treated as a trial that expires at TRACK_INSTALL_DATE + 90 days (configurable at build)
227+ % a present license that is not valid gets treated as an expired license.
228+ % a valid unexpired license gets treated as valid
229+ % a valid expired license gets treated as expired. license gets treated as valid, and we don not
230+ %
111231process_license (<<" " >>) ->
112232 {error , invalid_json };
113233process_license (LicJson ) ->
@@ -125,10 +245,9 @@ process_license(LicJson) ->
125245 <<" commercial" >> ->
126246 case ej :get ({<<" grace_period" >>}, LicDetails ) of
127247 true ->
128- {ok , commercial_grace_period , ExpDate ,
129- get_alert_message (commercial_grace_period , ExpDate ), CustomerName , LicenseId };
248+ {ok , commercial_grace_period , ExpDate , get_alert_message (commercial_grace_period , ExpDate ), CustomerName , LicenseId };
130249 _ ->
131- { ok , commercial_expired , ExpDate , get_alert_message (commercial_expired , ExpDate ), CustomerName , LicenseId }
250+ {ok , commercial_expired , ExpDate , get_alert_message (commercial_expired , ExpDate ), CustomerName , LicenseId }
132251 end ;
133252 _ ->
134253 {ok , trial_expired , ExpDate , get_alert_message (trial_expired , ExpDate ), CustomerName , LicenseId }
@@ -148,6 +267,7 @@ process_license(LicJson) ->
148267 {error , invalid_response }
149268 end .
150269
270+
151271get_alert_message (Type , ExpDate ) ->
152272 case Type of
153273 trial_expired ->
0 commit comments